codesesh 0.9.1 → 0.11.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-BIXOP5QX.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(
@@ -1308,169 +1049,452 @@ function resolveAgentWatchTargets(agentName) {
1308
1049
  { root: roots.opencodeRoot, path: join2(roots.opencodeRoot, "opencode.db") },
1309
1050
  { root: "data/opencode", path: "data/opencode/opencode.db" }
1310
1051
  ];
1052
+ case "zcode":
1053
+ return roots.zcodeRoot ? [{ root: roots.zcodeRoot, path: join2(roots.zcodeRoot, "cli", "db", "db.sqlite") }] : [];
1311
1054
  default:
1312
1055
  return [];
1313
1056
  }
1314
1057
  }
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();
1058
+ var SessionWatcher = class {
1346
1059
  watchers = [];
1347
1060
  fallbackWatchScopes = /* @__PURE__ */ new Map();
1348
1061
  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);
1062
+ listeners = /* @__PURE__ */ new Set();
1063
+ /** Register a listener fired (after write-stability polling) with the changed agent set. */
1064
+ onAgentsChanged(cb) {
1065
+ this.listeners.add(cb);
1457
1066
  return () => {
1458
- this.scanStatusListeners.delete(listener);
1067
+ this.listeners.delete(cb);
1459
1068
  };
1460
1069
  }
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);
1070
+ /** Begin watching the given agent names' data directories. */
1071
+ start(agentNames) {
1072
+ const scopesByRoot = /* @__PURE__ */ new Map();
1073
+ for (const agentName of agentNames) {
1074
+ const watchTargets = resolveAgentWatchTargets(agentName);
1075
+ if (watchTargets.length === 0) {
1076
+ appLogger.debug("watch.skip", { agent: agentName });
1077
+ continue;
1078
+ }
1079
+ for (const target of watchTargets) {
1080
+ const watchRootPath = closestWatchablePath(target.root ?? target.path);
1081
+ if (!watchRootPath) continue;
1082
+ let rootPath;
1083
+ try {
1084
+ rootPath = getWatchRoot(watchRootPath);
1085
+ } catch (error) {
1086
+ this.reportWatchError("watch.resolve.error", { path: watchRootPath, error });
1087
+ continue;
1088
+ }
1089
+ const targetPath = toAbsolutePath(target.path);
1090
+ const scopes = scopesByRoot.get(rootPath) ?? [];
1091
+ if (!scopes.some((scope) => scope.agentName === agentName && scope.targetPath === targetPath)) {
1092
+ scopes.push({ agentName, targetPath });
1093
+ }
1094
+ scopesByRoot.set(rootPath, scopes);
1471
1095
  }
1472
1096
  }
1473
- this.stablePaths.clear();
1097
+ for (const [rootPath, scopes] of scopesByRoot.entries()) {
1098
+ const agents = Array.from(new Set(scopes.map((scope) => scope.agentName)));
1099
+ appLogger.info("watch.start", {
1100
+ root: rootPath,
1101
+ agents,
1102
+ targets: scopes.map((scope) => ({
1103
+ agent: scope.agentName,
1104
+ path: scope.targetPath
1105
+ }))
1106
+ });
1107
+ if (isRecursiveWatchSupported()) {
1108
+ const started = this.watchDirectory(rootPath, scopes, true);
1109
+ if (started) {
1110
+ continue;
1111
+ }
1112
+ }
1113
+ this.watchDirectoryTree(rootPath, scopes);
1114
+ }
1115
+ }
1116
+ /** Stop all watchers and clear pending stability polls. */
1117
+ async dispose() {
1118
+ for (const state of this.stablePaths.values()) {
1119
+ if (state.timer) {
1120
+ clearTimeout(state.timer);
1121
+ }
1122
+ }
1123
+ this.stablePaths.clear();
1124
+ await Promise.all(this.watchers.map((watcher) => watcher.close()));
1125
+ this.watchers = [];
1126
+ this.fallbackWatchScopes.clear();
1127
+ this.listeners.clear();
1128
+ }
1129
+ watchDirectory(path, scopes, recursive) {
1130
+ try {
1131
+ const watcher = watch(path, { recursive }, (eventType, filename) => {
1132
+ queueMicrotask(() => {
1133
+ try {
1134
+ const activeScopes = recursive ? scopes : this.fallbackWatchScopes.get(path) ?? scopes;
1135
+ this.handleWatchEvent(path, activeScopes, eventType, filename);
1136
+ if (!recursive) {
1137
+ this.watchNewDirectories(path, filename, activeScopes);
1138
+ }
1139
+ } catch (error) {
1140
+ this.reportWatchError("watch.event.error", { path, recursive, error });
1141
+ }
1142
+ });
1143
+ });
1144
+ watcher.on("error", (error) => {
1145
+ this.reportWatchError("watch.error", { path, recursive, error });
1146
+ });
1147
+ this.watchers.push(watcher);
1148
+ return true;
1149
+ } catch (error) {
1150
+ if (recursive && isRecursiveWatchUnavailable(error)) {
1151
+ appLogger.warn("watch.recursive_unavailable", { path, error });
1152
+ return false;
1153
+ }
1154
+ this.reportWatchError("watch.start.error", { path, recursive, error });
1155
+ return false;
1156
+ }
1157
+ }
1158
+ watchDirectoryTree(rootPath, scopes) {
1159
+ const pending = [rootPath];
1160
+ while (pending.length > 0) {
1161
+ const dirPath = pending.pop();
1162
+ this.watchFallbackDirectory(dirPath, scopes);
1163
+ try {
1164
+ for (const entry of readdirSync2(dirPath, { withFileTypes: true })) {
1165
+ if (entry.isDirectory()) {
1166
+ pending.push(join2(dirPath, entry.name));
1167
+ }
1168
+ }
1169
+ } catch (error) {
1170
+ this.reportWatchError("watch.scan.error", { path: dirPath, error });
1171
+ }
1172
+ }
1173
+ }
1174
+ watchFallbackDirectory(path, scopes) {
1175
+ const existingScopes = this.fallbackWatchScopes.get(path);
1176
+ if (existingScopes) {
1177
+ mergeScopes(existingScopes, scopes);
1178
+ return;
1179
+ }
1180
+ const storedScopes = [...scopes];
1181
+ this.fallbackWatchScopes.set(path, storedScopes);
1182
+ if (!this.watchDirectory(path, storedScopes, false)) {
1183
+ this.fallbackWatchScopes.delete(path);
1184
+ }
1185
+ }
1186
+ watchNewDirectories(watchPath, filename, scopes) {
1187
+ const path = resolveWatchEventPath(watchPath, filename);
1188
+ try {
1189
+ if (statSync2(path).isDirectory()) {
1190
+ this.watchDirectoryTree(path, scopes);
1191
+ }
1192
+ } catch {
1193
+ }
1194
+ }
1195
+ handleWatchEvent(watchPath, scopes, eventType, filename) {
1196
+ const changedPath = resolveWatchEventPath(watchPath, filename);
1197
+ const agentNames = new Set(
1198
+ scopes.filter((scope) => isRelatedPath(changedPath, scope.targetPath)).map((scope) => scope.agentName)
1199
+ );
1200
+ if (agentNames.size === 0) {
1201
+ return;
1202
+ }
1203
+ appLogger.debug("watch.event", {
1204
+ event: eventType,
1205
+ path: changedPath,
1206
+ agents: Array.from(agentNames)
1207
+ });
1208
+ this.waitForStablePath(changedPath, agentNames);
1209
+ }
1210
+ waitForStablePath(path, agentNames) {
1211
+ const existing = this.stablePaths.get(path);
1212
+ if (existing) {
1213
+ for (const agentName of agentNames) {
1214
+ existing.agentNames.add(agentName);
1215
+ }
1216
+ return;
1217
+ }
1218
+ const state = {
1219
+ path,
1220
+ agentNames: new Set(agentNames),
1221
+ lastMtimeMs: null,
1222
+ lastSize: null,
1223
+ stableSince: Date.now(),
1224
+ timer: null
1225
+ };
1226
+ this.stablePaths.set(path, state);
1227
+ this.pollStablePath(path);
1228
+ }
1229
+ pollStablePath(path) {
1230
+ const state = this.stablePaths.get(path);
1231
+ if (!state) {
1232
+ return;
1233
+ }
1234
+ let size;
1235
+ let mtimeMs;
1236
+ try {
1237
+ const stat = statSync2(path);
1238
+ size = stat.size;
1239
+ mtimeMs = stat.mtimeMs;
1240
+ } catch {
1241
+ this.stablePaths.delete(path);
1242
+ this.emitAgentsChanged(state.agentNames);
1243
+ return;
1244
+ }
1245
+ const now = Date.now();
1246
+ const unchanged = state.lastSize === size && state.lastMtimeMs === mtimeMs;
1247
+ if (!unchanged) {
1248
+ state.lastSize = size;
1249
+ state.lastMtimeMs = mtimeMs;
1250
+ state.stableSince = now;
1251
+ }
1252
+ if (unchanged && now - state.stableSince >= WRITE_STABILITY_THRESHOLD_MS) {
1253
+ this.stablePaths.delete(path);
1254
+ this.emitAgentsChanged(state.agentNames);
1255
+ return;
1256
+ }
1257
+ state.timer = setTimeout(() => this.pollStablePath(path), WRITE_STABILITY_POLL_MS);
1258
+ }
1259
+ emitAgentsChanged(agentNames) {
1260
+ if (agentNames.size === 0) return;
1261
+ for (const listener of this.listeners) {
1262
+ listener(new Set(agentNames));
1263
+ }
1264
+ }
1265
+ reportWatchError(event, data) {
1266
+ appLogger.error(event, data);
1267
+ console.error("[watch] File watcher failed:", data.error);
1268
+ }
1269
+ };
1270
+
1271
+ // src/live-scan.ts
1272
+ var REFRESH_DEBOUNCE_MS = 200;
1273
+ var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
1274
+ var PENDING_REFRESH_DELAY_MS = 100;
1275
+ var NEW_SESSION_EVENT_WINDOW_MS = 250;
1276
+ var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
1277
+ function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
1278
+ const { changes, removedSessionIds, counts } = computeSessionDiff(
1279
+ previousSessions,
1280
+ nextSessions,
1281
+ candidateChangedIds,
1282
+ sessionSignature
1283
+ );
1284
+ if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) {
1285
+ return { event: null, changedSessions: changes, removedSessionIds };
1286
+ }
1287
+ return {
1288
+ changedSessions: changes,
1289
+ removedSessionIds,
1290
+ event: {
1291
+ type: "sessions-updated",
1292
+ changedAgents: [agentName],
1293
+ newSessions: counts.new,
1294
+ updatedSessions: counts.updated,
1295
+ removedSessions: counts.removed,
1296
+ totalSessions: nextSessions.length,
1297
+ timestamp: Date.now(),
1298
+ changedSessionHeads: changes.map(({ session }) => ({ agentName, session })),
1299
+ removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
1300
+ }
1301
+ };
1302
+ }
1303
+ function restoreAgentCacheMeta(agent, meta) {
1304
+ agent.setSessionMetaMap(new Map(Object.entries(meta)));
1305
+ }
1306
+ function mergeEvents(previous, next) {
1307
+ const changedSessionHeads = /* @__PURE__ */ new Map();
1308
+ const removedSessionRefs = /* @__PURE__ */ new Map();
1309
+ const sessionKey = (agentName, sessionId) => `${agentName}\0${sessionId}`;
1310
+ const addChanged = (item) => {
1311
+ const key = sessionKey(item.agentName, item.session.id);
1312
+ removedSessionRefs.delete(key);
1313
+ changedSessionHeads.set(key, item);
1314
+ };
1315
+ const addRemoved = (item) => {
1316
+ const key = sessionKey(item.agentName, item.sessionId);
1317
+ changedSessionHeads.delete(key);
1318
+ removedSessionRefs.set(key, item);
1319
+ };
1320
+ for (const item of previous.changedSessionHeads) addChanged(item);
1321
+ for (const item of previous.removedSessionRefs) addRemoved(item);
1322
+ for (const item of next.changedSessionHeads) addChanged(item);
1323
+ for (const item of next.removedSessionRefs) addRemoved(item);
1324
+ return {
1325
+ type: "sessions-updated",
1326
+ changedAgents: Array.from(/* @__PURE__ */ new Set([...previous.changedAgents, ...next.changedAgents])),
1327
+ newSessions: previous.newSessions + next.newSessions,
1328
+ updatedSessions: previous.updatedSessions + next.updatedSessions,
1329
+ removedSessions: previous.removedSessions + next.removedSessions,
1330
+ totalSessions: next.totalSessions,
1331
+ timestamp: next.timestamp,
1332
+ changedSessionHeads: [...changedSessionHeads.values()],
1333
+ removedSessionRefs: [...removedSessionRefs.values()]
1334
+ };
1335
+ }
1336
+ var LiveScanStore = class {
1337
+ constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}, storeOptions = {}) {
1338
+ this.watchEnabled = watchEnabled;
1339
+ this.scanOptions = scanOptions;
1340
+ this.startupScanOptions = startupScanOptions;
1341
+ this.storeOptions = storeOptions;
1342
+ }
1343
+ watchEnabled;
1344
+ scanOptions;
1345
+ startupScanOptions;
1346
+ storeOptions;
1347
+ agents = [];
1348
+ byAgent = {};
1349
+ sessions = [];
1350
+ listeners = /* @__PURE__ */ new Set();
1351
+ scanStatusListeners = /* @__PURE__ */ new Set();
1352
+ scanStatus = {
1353
+ active: false,
1354
+ phase: "idle",
1355
+ pendingAgents: [],
1356
+ scanningAgents: [],
1357
+ completedAgents: [],
1358
+ agentStatuses: {},
1359
+ totalAgents: 0,
1360
+ updatedAt: Date.now()
1361
+ };
1362
+ refreshTimers = /* @__PURE__ */ new Map();
1363
+ refreshTimestamps = /* @__PURE__ */ new Map();
1364
+ refreshInFlight = /* @__PURE__ */ new Set();
1365
+ pendingRefreshes = /* @__PURE__ */ new Set();
1366
+ pendingRefreshPathCounts = /* @__PURE__ */ new Map();
1367
+ watcher = null;
1368
+ pendingEvent = null;
1369
+ pendingEventTimer = null;
1370
+ backgroundRefreshTimer = null;
1371
+ searchIndexWorker = null;
1372
+ pendingSearchIndexJobs = [];
1373
+ shuttingDown = false;
1374
+ async initialize() {
1375
+ const startedAt = performance.now();
1376
+ const deferInitialRefresh = this.storeOptions.deferInitialRefresh === true;
1377
+ appLogger.info("scan.initial.start", {
1378
+ watch_enabled: this.watchEnabled,
1379
+ agents: this.scanOptions.agents,
1380
+ use_cache: this.scanOptions.useCache ?? true,
1381
+ startup_from: this.startupScanOptions.from,
1382
+ startup_to: this.startupScanOptions.to,
1383
+ deferred: deferInitialRefresh || void 0
1384
+ });
1385
+ const initialResult = await scanSessions({
1386
+ ...this.scanOptions,
1387
+ ...deferInitialRefresh ? this.startupScanOptions : {},
1388
+ useCache: this.scanOptions.useCache ?? true,
1389
+ smartRefresh: false,
1390
+ cacheOnly: deferInitialRefresh,
1391
+ writeCache: deferInitialRefresh ? false : this.scanOptions.writeCache,
1392
+ smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
1393
+ includeSmartTags: deferInitialRefresh ? false : void 0
1394
+ });
1395
+ this.applyScanResult(initialResult);
1396
+ const indexStartedAt = performance.now();
1397
+ if (!deferInitialRefresh) {
1398
+ await this.enqueueSearchIndexJobs(
1399
+ "scan.initial",
1400
+ this.buildFullSearchIndexJobs("scan.initial")
1401
+ );
1402
+ }
1403
+ const indexDuration = performance.now() - indexStartedAt;
1404
+ appLogger.info("scan.initial.done", {
1405
+ duration_ms: Math.round(performance.now() - startedAt),
1406
+ index_ms: deferInitialRefresh ? void 0 : Math.round(indexDuration),
1407
+ deferred: deferInitialRefresh || void 0,
1408
+ sessions: this.sessions.length,
1409
+ agents: Object.fromEntries(
1410
+ Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
1411
+ ),
1412
+ agent_timings: initialResult.timings ? Object.fromEntries(
1413
+ Object.entries(initialResult.timings).map(([name, t]) => [
1414
+ name,
1415
+ {
1416
+ total_ms: Math.round(t.total),
1417
+ cache_load_ms: t.cacheLoad != null ? Math.round(t.cacheLoad) : void 0,
1418
+ check_changes_ms: t.checkChanges != null ? Math.round(t.checkChanges) : void 0,
1419
+ scan_ms: t.scan != null ? Math.round(t.scan) : void 0,
1420
+ identity_ms: t.identity != null ? Math.round(t.identity) : void 0,
1421
+ tags_ms: t.tags != null ? Math.round(t.tags) : void 0
1422
+ }
1423
+ ])
1424
+ ) : void 0
1425
+ });
1426
+ if (this.watchEnabled) {
1427
+ this.watcher = new SessionWatcher();
1428
+ this.watcher.onAgentsChanged((agentNames) => {
1429
+ for (const agentName of agentNames) {
1430
+ this.pendingRefreshPathCounts.set(
1431
+ agentName,
1432
+ (this.pendingRefreshPathCounts.get(agentName) ?? 0) + 1
1433
+ );
1434
+ const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
1435
+ this.scheduleRefresh(agentName, delayMs);
1436
+ }
1437
+ });
1438
+ this.watcher.start(this.agents.map((agent) => agent.name));
1439
+ }
1440
+ }
1441
+ startBackgroundRefresh() {
1442
+ if (this.backgroundRefreshTimer) {
1443
+ return;
1444
+ }
1445
+ const agentNames = this.agents.map((agent) => agent.name);
1446
+ this.startScanBatch(agentNames, "scanning");
1447
+ this.backgroundRefreshTimer = setTimeout(() => {
1448
+ this.backgroundRefreshTimer = null;
1449
+ for (const agentName of agentNames) {
1450
+ this.scheduleRefresh(agentName, 0);
1451
+ }
1452
+ if (agentNames.length === 0) {
1453
+ this.finishScanBatch();
1454
+ }
1455
+ }, 0);
1456
+ }
1457
+ getSnapshot() {
1458
+ return {
1459
+ sessions: this.sessions,
1460
+ byAgent: this.byAgent,
1461
+ agents: this.agents
1462
+ };
1463
+ }
1464
+ getScanStatus() {
1465
+ return {
1466
+ type: "scan-status",
1467
+ ...this.scanStatus,
1468
+ pendingAgents: [...this.scanStatus.pendingAgents],
1469
+ scanningAgents: [...this.scanStatus.scanningAgents],
1470
+ completedAgents: [...this.scanStatus.completedAgents],
1471
+ agentStatuses: Object.fromEntries(
1472
+ Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
1473
+ agentName,
1474
+ { ...status }
1475
+ ])
1476
+ )
1477
+ };
1478
+ }
1479
+ subscribe(listener) {
1480
+ this.listeners.add(listener);
1481
+ return () => {
1482
+ this.listeners.delete(listener);
1483
+ };
1484
+ }
1485
+ subscribeScanStatus(listener) {
1486
+ this.scanStatusListeners.add(listener);
1487
+ return () => {
1488
+ this.scanStatusListeners.delete(listener);
1489
+ };
1490
+ }
1491
+ async shutdown() {
1492
+ this.shuttingDown = true;
1493
+ for (const timer of this.refreshTimers.values()) {
1494
+ clearTimeout(timer);
1495
+ }
1496
+ this.refreshTimers.clear();
1497
+ this.pendingRefreshPathCounts.clear();
1474
1498
  if (this.pendingEventTimer) {
1475
1499
  clearTimeout(this.pendingEventTimer);
1476
1500
  this.pendingEventTimer = null;
@@ -1488,9 +1512,10 @@ var LiveScanStore = class {
1488
1512
  }
1489
1513
  this.pendingSearchIndexJobs = [];
1490
1514
  this.pendingEvent = null;
1491
- await Promise.all(this.watchers.map((watcher) => watcher.close()));
1492
- this.watchers = [];
1493
- this.fallbackWatchScopes.clear();
1515
+ if (this.watcher) {
1516
+ await this.watcher.dispose();
1517
+ this.watcher = null;
1518
+ }
1494
1519
  }
1495
1520
  emit(event) {
1496
1521
  if (this.pendingEvent || event.newSessions > 0) {
@@ -1674,14 +1699,14 @@ var LiveScanStore = class {
1674
1699
  }
1675
1700
  getSearchIndexWorkerUrl() {
1676
1701
  const workerUrl = new URL("./search-index-worker.js", import.meta.url);
1677
- if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
1702
+ if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath2(workerUrl))) {
1678
1703
  return null;
1679
1704
  }
1680
1705
  return workerUrl;
1681
1706
  }
1682
1707
  getSmartTagWorkerUrl() {
1683
1708
  const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
1684
- if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
1709
+ if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath2(workerUrl))) {
1685
1710
  return null;
1686
1711
  }
1687
1712
  return workerUrl;
@@ -1861,9 +1886,6 @@ var LiveScanStore = class {
1861
1886
  applyFilters(sessions) {
1862
1887
  return filterSessions(sessions, { ...this.scanOptions, ...this.startupScanOptions });
1863
1888
  }
1864
- canSyncSources(agent) {
1865
- return Boolean(agent.listSessionSources && agent.scanSessionSource);
1866
- }
1867
1889
  async refreshInitialIndex() {
1868
1890
  const startedAt = performance.now();
1869
1891
  const context = "scan.initial.background";
@@ -1881,195 +1903,6 @@ var LiveScanStore = class {
1881
1903
  console.error("[search] Background index sync failed:", error);
1882
1904
  }
1883
1905
  }
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
1906
  scheduleRefresh(agentName, delayMs = REFRESH_DEBOUNCE_MS) {
2074
1907
  appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: delayMs });
2075
1908
  const existing = this.refreshTimers.get(agentName);
@@ -2149,7 +1982,7 @@ var LiveScanStore = class {
2149
1982
  nextSessions = fullScanSessions;
2150
1983
  scanDuration = performance.now() - scanStartedAt;
2151
1984
  this.refreshTimestamps.set(agentName, Date.now());
2152
- } else if (cached && this.canSyncSources(agent)) {
1985
+ } else if (cached && agent instanceof FileSystemSessionSource) {
2153
1986
  const scanStartedAt = performance.now();
2154
1987
  const result = await this.scanAgentInWorker(
2155
1988
  agent,
@@ -2162,7 +1995,7 @@ var LiveScanStore = class {
2162
1995
  }
2163
1996
  );
2164
1997
  nextSessions = result.sessions;
2165
- agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
1998
+ agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2166
1999
  preciseChangedIds = result.changedIds ?? [];
2167
2000
  usedIncrementalScan = true;
2168
2001
  persistenceDiff = buildRefreshDiff(
@@ -2179,7 +2012,7 @@ var LiveScanStore = class {
2179
2012
  duration_ms: Math.round(performance.now() - startedAt)
2180
2013
  });
2181
2014
  }
2182
- } else if (refreshBaseline.length > 0 && agent.checkForChanges && agent.incrementalScan) {
2015
+ } else if (refreshBaseline.length > 0) {
2183
2016
  const checkStartedAt = performance.now();
2184
2017
  const checkResult = await Promise.resolve(
2185
2018
  agent.checkForChanges(cacheTimestamp, refreshBaseline)
@@ -2212,7 +2045,7 @@ var LiveScanStore = class {
2212
2045
  const scanStartedAt = performance.now();
2213
2046
  const result = await this.scanAgentInWorker(agent, previousSessions, null, {});
2214
2047
  nextSessions = result.sessions;
2215
- agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
2048
+ agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2216
2049
  fullScanSessions = attachMissingProjectIdentities(nextSessions);
2217
2050
  nextSessions = fullScanSessions;
2218
2051
  scanDuration = performance.now() - scanStartedAt;
@@ -2445,7 +2278,7 @@ var main = defineCommand({
2445
2278
  log_path: appLogger.getLogPath()
2446
2279
  });
2447
2280
  if (clearCache) {
2448
- const { clearCache: clear } = await import("./dist-VEIV33FR.js");
2281
+ const { clearCache: clear } = await import("./dist-T737I76S.js");
2449
2282
  clear();
2450
2283
  appLogger.info("cache.clear");
2451
2284
  console.log("Cache cleared.");