sidekick-shared 0.27.6 → 0.28.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.
Files changed (269) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +58 -18
  3. package/dist/browser.d.ts +13 -2
  4. package/dist/browser.js +30 -2
  5. package/dist/claudePaths.d.ts +11 -0
  6. package/dist/claudePaths.js +61 -0
  7. package/dist/claudeProfiles.d.ts +2 -9
  8. package/dist/claudeProfiles.js +10 -25
  9. package/dist/codexPaths.d.ts +22 -0
  10. package/dist/codexPaths.js +67 -0
  11. package/dist/codexProfiles.d.ts +1 -4
  12. package/dist/codexProfiles.js +5 -43
  13. package/dist/doctor.js +2 -2
  14. package/dist/esm/accountChangeSignal.js +15 -0
  15. package/dist/esm/accountChanges.js +152 -0
  16. package/dist/esm/accountHealth.js +288 -0
  17. package/dist/esm/accountLaunch.js +145 -0
  18. package/dist/esm/accountLease.js +60 -0
  19. package/dist/esm/accountManager.js +606 -0
  20. package/dist/esm/accountRegistry.js +214 -0
  21. package/dist/esm/accountStatus.js +51 -0
  22. package/dist/esm/accountSwitch.js +74 -0
  23. package/dist/esm/accountSync.js +156 -0
  24. package/dist/esm/accountSyncTypes.js +4 -0
  25. package/dist/esm/accounts.js +1291 -0
  26. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  27. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  28. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  29. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  30. package/dist/esm/aggregation/costProvenance.js +31 -0
  31. package/dist/esm/aggregation/snapshot.js +161 -0
  32. package/dist/esm/aggregation/types.js +5 -0
  33. package/dist/esm/analytics/codeImpact.js +18 -0
  34. package/dist/esm/analytics/compactionLedger.js +37 -0
  35. package/dist/esm/analytics/qualityScore.js +112 -0
  36. package/dist/esm/autoSwitch.js +185 -0
  37. package/dist/esm/browser.js +34 -0
  38. package/dist/esm/claudeCredentialDoctor.js +239 -0
  39. package/dist/esm/claudeCredentialLedger.js +91 -0
  40. package/dist/esm/claudeCredentials.js +111 -0
  41. package/dist/esm/claudeLiveGuard.js +43 -0
  42. package/dist/esm/claudePaths.js +22 -0
  43. package/dist/esm/claudeProfiles.js +142 -0
  44. package/dist/esm/claudeRefreshLock.js +106 -0
  45. package/dist/esm/codexAuth.js +101 -0
  46. package/dist/esm/codexCredentialDoctor.js +265 -0
  47. package/dist/esm/codexCredentialStore.js +164 -0
  48. package/dist/esm/codexKeepAlive.js +112 -0
  49. package/dist/esm/codexPaths.js +172 -0
  50. package/dist/esm/codexProfiles.js +1251 -0
  51. package/dist/esm/codexQuota.js +564 -0
  52. package/dist/esm/codexQuotaWatcher.js +363 -0
  53. package/dist/esm/context/composer.js +71 -0
  54. package/dist/esm/context/sessionContext.js +546 -0
  55. package/dist/esm/credentialIO.js +283 -0
  56. package/dist/esm/credentials.js +49 -0
  57. package/dist/esm/doctor.js +305 -0
  58. package/dist/esm/ensureDefaultAccounts.js +62 -0
  59. package/dist/esm/errorHistory.js +56 -0
  60. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  61. package/dist/esm/extractors/gatherAssets.js +32 -0
  62. package/dist/esm/extractors/sessionAssets.js +176 -0
  63. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  64. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  65. package/dist/esm/extractors/tokenUsage.js +41 -0
  66. package/dist/esm/extractors/toolCall.js +142 -0
  67. package/dist/esm/failingTools.js +65 -0
  68. package/dist/esm/fileHead.js +27 -0
  69. package/dist/esm/formatters/eventHighlighter.js +332 -0
  70. package/dist/esm/formatters/noiseClassifier.js +209 -0
  71. package/dist/esm/formatters/sessionDump.js +328 -0
  72. package/dist/esm/formatters/toolSummary.js +226 -0
  73. package/dist/esm/formatting.js +82 -0
  74. package/dist/esm/handoffUrl.js +23 -0
  75. package/dist/esm/historicalStore.js +305 -0
  76. package/dist/esm/humanPrompt.js +88 -0
  77. package/dist/esm/index.js +213 -0
  78. package/dist/esm/keepAlive.js +189 -0
  79. package/dist/esm/liveCredentialGuard.js +33 -0
  80. package/dist/esm/modelAliases.js +51 -0
  81. package/dist/esm/modelCatalog.js +139 -0
  82. package/dist/esm/modelContext.js +280 -0
  83. package/dist/esm/modelInfo.js +832 -0
  84. package/dist/esm/multiProviderQuotaService.js +450 -0
  85. package/dist/esm/node.js +46 -0
  86. package/dist/esm/observedContextWindows.js +165 -0
  87. package/dist/esm/observedSessionCollector.js +939 -0
  88. package/dist/esm/parsers/changelogParser.js +71 -0
  89. package/dist/esm/parsers/codexHistory.js +89 -0
  90. package/dist/esm/parsers/codexParser.js +906 -0
  91. package/dist/esm/parsers/debugLogParser.js +126 -0
  92. package/dist/esm/parsers/jsonl.js +66 -0
  93. package/dist/esm/parsers/mcpToolName.js +28 -0
  94. package/dist/esm/parsers/openCodeParser.js +625 -0
  95. package/dist/esm/parsers/planExtractor.js +335 -0
  96. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  97. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  98. package/dist/esm/parsers/subagentScanner.js +360 -0
  99. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  100. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  101. package/dist/esm/paths.js +134 -0
  102. package/dist/esm/peakHours.js +194 -0
  103. package/dist/esm/phrases.js +1330 -0
  104. package/dist/esm/pricingCatalog.js +247 -0
  105. package/dist/esm/processDetection.js +200 -0
  106. package/dist/esm/projectMigration.js +59 -0
  107. package/dist/esm/promptHistory.js +1221 -0
  108. package/dist/esm/providerFailure.js +362 -0
  109. package/dist/esm/providerIds.js +6 -0
  110. package/dist/esm/providerQuota.js +1 -0
  111. package/dist/esm/providerServiceStatus.js +145 -0
  112. package/dist/esm/providerServiceStatusTypes.js +1 -0
  113. package/dist/esm/providerStatus.js +117 -0
  114. package/dist/esm/providers/claudeCode.js +575 -0
  115. package/dist/esm/providers/claudeListing.js +121 -0
  116. package/dist/esm/providers/codex.js +1034 -0
  117. package/dist/esm/providers/codexDatabase.js +297 -0
  118. package/dist/esm/providers/codexRollouts.js +215 -0
  119. package/dist/esm/providers/detect.js +185 -0
  120. package/dist/esm/providers/diagnostics.js +83 -0
  121. package/dist/esm/providers/directoryListingCache.js +206 -0
  122. package/dist/esm/providers/factory.js +54 -0
  123. package/dist/esm/providers/openCode.js +2008 -0
  124. package/dist/esm/providers/openCodeDatabase.js +419 -0
  125. package/dist/esm/providers/rolloutWalker.js +252 -0
  126. package/dist/esm/providers/types.js +12 -0
  127. package/dist/esm/quota.js +182 -0
  128. package/dist/esm/quotaHistory.js +364 -0
  129. package/dist/esm/quotaPoller.js +230 -0
  130. package/dist/esm/quotaPresentation.js +97 -0
  131. package/dist/esm/quotaResolver.js +231 -0
  132. package/dist/esm/quotaSnapshots.js +130 -0
  133. package/dist/esm/quotaThresholds.js +85 -0
  134. package/dist/esm/readers/decisions.js +24 -0
  135. package/dist/esm/readers/handoff.js +20 -0
  136. package/dist/esm/readers/helpers.js +27 -0
  137. package/dist/esm/readers/history.js +9 -0
  138. package/dist/esm/readers/notes.js +48 -0
  139. package/dist/esm/readers/plans.js +225 -0
  140. package/dist/esm/readers/tasks.js +19 -0
  141. package/dist/esm/report/htmlHelpers.js +280 -0
  142. package/dist/esm/report/htmlReportGenerator.js +704 -0
  143. package/dist/esm/report/index.js +7 -0
  144. package/dist/esm/report/logo.js +2 -0
  145. package/dist/esm/report/openBrowser.js +19 -0
  146. package/dist/esm/report/sessionReportInputs.js +33 -0
  147. package/dist/esm/report/transcriptParser.js +83 -0
  148. package/dist/esm/report/types.js +4 -0
  149. package/dist/esm/schemas/accountManager.js +204 -0
  150. package/dist/esm/schemas/accountStatus.js +25 -0
  151. package/dist/esm/schemas/assistantTurn.js +74 -0
  152. package/dist/esm/schemas/index.js +25 -0
  153. package/dist/esm/schemas/observedSessionV1.js +77 -0
  154. package/dist/esm/schemas/providerFailure.js +37 -0
  155. package/dist/esm/schemas/quota.js +132 -0
  156. package/dist/esm/schemas/quotaHistory.js +40 -0
  157. package/dist/esm/schemas/sessionEvent.js +216 -0
  158. package/dist/esm/schemas/stateFile.js +75 -0
  159. package/dist/esm/schemas/tokenEstimation.js +7 -0
  160. package/dist/esm/schemas/transcript.js +86 -0
  161. package/dist/esm/schemas/usageNormalization.js +38 -0
  162. package/dist/esm/search/advancedFilter.js +197 -0
  163. package/dist/esm/search/sessionSearch.js +66 -0
  164. package/dist/esm/sessionFingerprint.js +40 -0
  165. package/dist/esm/sessionMonitor.js +198 -0
  166. package/dist/esm/sessionPreviews.js +522 -0
  167. package/dist/esm/sessionPromptHistory.js +254 -0
  168. package/dist/esm/sessionPromptSignals.js +364 -0
  169. package/dist/esm/sessionStats.js +160 -0
  170. package/dist/esm/sessionTokenTotals.js +98 -0
  171. package/dist/esm/sessionTranscripts.js +49 -0
  172. package/dist/esm/stateFile.js +119 -0
  173. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  174. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  175. package/dist/esm/statusline/formatter.js +122 -0
  176. package/dist/esm/statusline/index.js +8 -0
  177. package/dist/esm/terminalSync.js +95 -0
  178. package/dist/esm/testing/credentialGuard.js +197 -0
  179. package/dist/esm/testing/index.js +12 -0
  180. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  181. package/dist/esm/tokenEstimation.js +67 -0
  182. package/dist/esm/tokenSummary.js +113 -0
  183. package/dist/esm/toolCategory.js +82 -0
  184. package/dist/esm/transcript.js +257 -0
  185. package/dist/esm/turns/assistantTurn.js +373 -0
  186. package/dist/esm/types/codex.js +23 -0
  187. package/dist/esm/types/decisionLog.js +5 -0
  188. package/dist/esm/types/historicalData.js +47 -0
  189. package/dist/esm/types/knowledgeNote.js +15 -0
  190. package/dist/esm/types/observedSessionV1.js +257 -0
  191. package/dist/esm/types/opencode.js +12 -0
  192. package/dist/esm/types/plan.js +7 -0
  193. package/dist/esm/types/sessionEvent.js +10 -0
  194. package/dist/esm/types/taskPersistence.js +12 -0
  195. package/dist/esm/usage/billingBlocks.js +148 -0
  196. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  197. package/dist/esm/usage/importSessionHistory.js +84 -0
  198. package/dist/esm/usage/subagentUsage.js +18 -0
  199. package/dist/esm/usage/usageEvents.js +402 -0
  200. package/dist/esm/usage/usageReports.js +202 -0
  201. package/dist/esm/usageNormalization.js +95 -0
  202. package/dist/esm/watchers/eventBridge.js +219 -0
  203. package/dist/esm/watchers/factory.js +63 -0
  204. package/dist/esm/watchers/jsonlTail.js +289 -0
  205. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  206. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  207. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  208. package/dist/esm/watchers/types.js +4 -0
  209. package/dist/esm/workspaceRoots.js +56 -0
  210. package/dist/esm/writers/atomic.js +375 -0
  211. package/dist/esm/writers/decisions.js +51 -0
  212. package/dist/esm/writers/notes.js +46 -0
  213. package/dist/esm/writers/tasks.js +63 -0
  214. package/dist/esm/zaiQuota.js +323 -0
  215. package/dist/esm/zaiQuotaApi.js +271 -0
  216. package/dist/esm/zaiQuotaWatcher.js +265 -0
  217. package/dist/fileHead.d.ts +22 -0
  218. package/dist/fileHead.js +33 -0
  219. package/dist/index.d.ts +16 -7
  220. package/dist/index.js +49 -15
  221. package/dist/node.d.ts +13 -1
  222. package/dist/node.js +43 -2
  223. package/dist/parsers/codexHistory.js +2 -2
  224. package/dist/parsers/codexParser.d.ts +12 -0
  225. package/dist/parsers/codexParser.js +34 -12
  226. package/dist/parsers/debugLogParser.js +2 -2
  227. package/dist/parsers/sessionPathResolver.js +5 -4
  228. package/dist/promptHistory.d.ts +32 -0
  229. package/dist/promptHistory.js +115 -59
  230. package/dist/providerFailure.d.ts +12 -6
  231. package/dist/providerFailure.js +45 -0
  232. package/dist/providers/claudeCode.js +7 -99
  233. package/dist/providers/claudeListing.d.ts +22 -0
  234. package/dist/providers/claudeListing.js +158 -0
  235. package/dist/providers/codex.d.ts +3 -1
  236. package/dist/providers/codex.js +26 -227
  237. package/dist/providers/codexRollouts.d.ts +44 -0
  238. package/dist/providers/codexRollouts.js +261 -0
  239. package/dist/providers/detect.js +4 -4
  240. package/dist/readers/plans.js +3 -3
  241. package/dist/schemas/index.d.ts +2 -0
  242. package/dist/schemas/index.js +7 -2
  243. package/dist/schemas/providerFailure.d.ts +154 -0
  244. package/dist/schemas/providerFailure.js +40 -0
  245. package/dist/sessionPromptHistory.d.ts +12 -7
  246. package/dist/sessionPromptHistory.js +15 -3
  247. package/dist/sessionPromptSignals.d.ts +9 -0
  248. package/dist/sessionPromptSignals.js +13 -0
  249. package/dist/testing/credentialGuard.d.ts +23 -0
  250. package/dist/testing/credentialGuard.js +233 -0
  251. package/dist/testing/index.d.ts +14 -0
  252. package/dist/testing/index.js +22 -0
  253. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  254. package/dist/testing/sessionLogFixtures.js +169 -0
  255. package/dist/tokenEstimation.d.ts +2 -0
  256. package/dist/tokenEstimation.js +21 -11
  257. package/dist/toolCategory.d.ts +18 -0
  258. package/dist/toolCategory.js +86 -0
  259. package/dist/types/codex.d.ts +4 -0
  260. package/dist/usage/usageEvents.d.ts +16 -0
  261. package/dist/usage/usageEvents.js +53 -9
  262. package/dist/watchers/jsonlTail.d.ts +26 -1
  263. package/dist/watchers/jsonlTail.js +136 -28
  264. package/dist/watchers/jsonlWatcher.js +7 -0
  265. package/dist/watchers/providerReaderWatcher.js +1 -0
  266. package/dist/watchers/types.d.ts +6 -0
  267. package/dist/workspaceRoots.d.ts +23 -0
  268. package/dist/workspaceRoots.js +93 -0
  269. package/package.json +22 -2
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Generic frequency counter with LRU eviction.
3
+ *
4
+ * Tracks word/key frequencies with count, firstSeen, lastSeen metadata.
5
+ * Used for tool name frequency, event type frequency, and summary word frequency.
6
+ *
7
+ * @module aggregation/FrequencyTracker
8
+ */
9
+ const DEFAULT_MAX_ENTRIES = 10_000;
10
+ export class FrequencyTracker {
11
+ entries = new Map();
12
+ maxEntries;
13
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
14
+ this.maxEntries = maxEntries;
15
+ }
16
+ /** Increment frequency for a key. */
17
+ increment(key, timestamp) {
18
+ const ts = timestamp ?? new Date().toISOString();
19
+ const existing = this.entries.get(key);
20
+ if (existing) {
21
+ existing.count++;
22
+ existing.lastSeen = ts;
23
+ // Move to end for LRU ordering (Map preserves insertion order)
24
+ this.entries.delete(key);
25
+ this.entries.set(key, existing);
26
+ }
27
+ else {
28
+ // Evict oldest if at capacity
29
+ if (this.entries.size >= this.maxEntries) {
30
+ const oldest = this.entries.keys().next().value;
31
+ if (oldest !== undefined) {
32
+ this.entries.delete(oldest);
33
+ }
34
+ }
35
+ this.entries.set(key, { count: 1, firstSeen: ts, lastSeen: ts });
36
+ }
37
+ }
38
+ /** Get the top N entries by count. */
39
+ getTopN(n) {
40
+ const sorted = Array.from(this.entries.entries())
41
+ .map(([key, data]) => ({ key, ...data }))
42
+ .sort((a, b) => b.count - a.count);
43
+ return sorted.slice(0, n);
44
+ }
45
+ /** Get all entries sorted by count descending. */
46
+ getAll() {
47
+ return this.getTopN(this.entries.size);
48
+ }
49
+ /** Get count for a specific key. */
50
+ getCount(key) {
51
+ return this.entries.get(key)?.count ?? 0;
52
+ }
53
+ /** Get total number of tracked keys. */
54
+ get size() {
55
+ return this.entries.size;
56
+ }
57
+ /** Reset all tracked frequencies. */
58
+ reset() {
59
+ this.entries.clear();
60
+ }
61
+ /** Serialize for snapshot persistence. */
62
+ serialize() {
63
+ return { entries: Array.from(this.entries.entries()) };
64
+ }
65
+ /** Restore from serialized state. */
66
+ restore(state) {
67
+ this.entries = new Map(state.entries);
68
+ }
69
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Rolling heatmap tracker using a circular buffer of minute-buckets.
3
+ *
4
+ * Records event timestamps into minute-resolution buckets for activity
5
+ * visualization. Older buckets age out as time advances.
6
+ *
7
+ * @module aggregation/HeatmapTracker
8
+ */
9
+ const DEFAULT_BUCKET_COUNT = 60;
10
+ /** Truncate a Date to minute resolution and return the minute-key (epoch minutes). */
11
+ function toMinuteKey(date) {
12
+ return Math.floor(date.getTime() / 60_000);
13
+ }
14
+ export class HeatmapTracker {
15
+ buckets = new Map(); // minuteKey -> count
16
+ bucketCount;
17
+ constructor(bucketCount = DEFAULT_BUCKET_COUNT) {
18
+ this.bucketCount = bucketCount;
19
+ }
20
+ /** Record an event at the given timestamp. */
21
+ record(timestamp) {
22
+ const date = new Date(timestamp);
23
+ if (isNaN(date.getTime()))
24
+ return;
25
+ const minuteKey = toMinuteKey(date);
26
+ this.buckets.set(minuteKey, (this.buckets.get(minuteKey) ?? 0) + 1);
27
+ this.prune(minuteKey);
28
+ }
29
+ /** Get the rolling window of buckets, filled with zeros for empty minutes. */
30
+ getBuckets() {
31
+ if (this.buckets.size === 0)
32
+ return [];
33
+ // Find the latest minute key
34
+ let latestKey = 0;
35
+ for (const key of this.buckets.keys()) {
36
+ if (key > latestKey)
37
+ latestKey = key;
38
+ }
39
+ const result = [];
40
+ const startKey = latestKey - this.bucketCount + 1;
41
+ for (let key = startKey; key <= latestKey; key++) {
42
+ result.push({
43
+ timestamp: new Date(key * 60_000).toISOString(),
44
+ count: this.buckets.get(key) ?? 0,
45
+ });
46
+ }
47
+ return result;
48
+ }
49
+ /** Get the maximum count in any bucket (for intensity scaling). */
50
+ getMaxCount() {
51
+ let max = 0;
52
+ for (const count of this.buckets.values()) {
53
+ if (count > max)
54
+ max = count;
55
+ }
56
+ return max;
57
+ }
58
+ /** Reset all tracked data. */
59
+ reset() {
60
+ this.buckets.clear();
61
+ }
62
+ /** Serialize for snapshot persistence. */
63
+ serialize() {
64
+ let oldest = Infinity;
65
+ const entries = [];
66
+ for (const [key, count] of this.buckets) {
67
+ entries.push({ minuteKey: key, count });
68
+ if (key < oldest)
69
+ oldest = key;
70
+ }
71
+ return { buckets: entries, oldestMinuteKey: oldest === Infinity ? 0 : oldest };
72
+ }
73
+ /** Restore from serialized state. */
74
+ restore(state) {
75
+ this.buckets.clear();
76
+ for (const { minuteKey, count } of state.buckets) {
77
+ this.buckets.set(minuteKey, count);
78
+ }
79
+ }
80
+ /** Prune buckets outside the rolling window. */
81
+ prune(latestKey) {
82
+ const cutoff = latestKey - this.bucketCount;
83
+ for (const key of this.buckets.keys()) {
84
+ if (key <= cutoff) {
85
+ this.buckets.delete(key);
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Simplified Drain algorithm for session event clustering.
3
+ *
4
+ * Tokenizes event summaries, groups by token count, clusters by shared prefix
5
+ * tokens, and replaces variable tokens with `<*>` to produce templates.
6
+ * Surfaces repetitive patterns like "Read src/<*>.ts" (count: 5).
7
+ *
8
+ * @module aggregation/PatternExtractor
9
+ */
10
+ const DEFAULT_MAX_CLUSTERS = 100;
11
+ const DEFAULT_MAX_DEPTH = 4;
12
+ const DEFAULT_SIMILARITY_THRESHOLD = 0.5;
13
+ export class PatternExtractor {
14
+ maxClusters;
15
+ maxDepth;
16
+ similarityThreshold;
17
+ // Group clusters by token count for efficient lookup
18
+ groups = new Map();
19
+ constructor(options) {
20
+ this.maxClusters = options?.maxClusters ?? DEFAULT_MAX_CLUSTERS;
21
+ this.maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
22
+ this.similarityThreshold = options?.similarityThreshold ?? DEFAULT_SIMILARITY_THRESHOLD;
23
+ }
24
+ /** Add a summary string for clustering. */
25
+ add(summary) {
26
+ const tokens = this.tokenize(summary);
27
+ if (tokens.length === 0)
28
+ return;
29
+ const key = tokens.length;
30
+ let group = this.groups.get(key);
31
+ if (!group) {
32
+ group = [];
33
+ this.groups.set(key, group);
34
+ }
35
+ // Find best matching cluster
36
+ let bestCluster = null;
37
+ let bestSim = 0;
38
+ for (const cluster of group) {
39
+ const sim = this.similarity(tokens, cluster);
40
+ if (sim > bestSim) {
41
+ bestSim = sim;
42
+ bestCluster = cluster;
43
+ }
44
+ }
45
+ if (bestCluster && bestSim >= this.similarityThreshold) {
46
+ // Merge into existing cluster
47
+ bestCluster.count++;
48
+ if (bestCluster.examples.length < 3) {
49
+ bestCluster.examples.push(summary);
50
+ }
51
+ // Update mask: mark differing positions as wildcards
52
+ for (let i = 0; i < tokens.length; i++) {
53
+ if (!bestCluster.mask[i] && tokens[i] !== bestCluster.tokens[i]) {
54
+ bestCluster.mask[i] = true;
55
+ }
56
+ }
57
+ }
58
+ else {
59
+ // Create new cluster (evict least-used if at capacity)
60
+ if (this.totalClusters() >= this.maxClusters) {
61
+ this.evictSmallest();
62
+ }
63
+ group.push({
64
+ tokens: [...tokens],
65
+ mask: new Array(tokens.length).fill(false),
66
+ count: 1,
67
+ examples: [summary],
68
+ });
69
+ }
70
+ }
71
+ /** Get all patterns sorted by frequency. */
72
+ getPatterns() {
73
+ const result = [];
74
+ for (const group of this.groups.values()) {
75
+ for (const cluster of group) {
76
+ if (cluster.count < 2)
77
+ continue; // Only show patterns with 2+ matches
78
+ result.push({
79
+ template: this.buildTemplate(cluster),
80
+ count: cluster.count,
81
+ examples: [...cluster.examples],
82
+ });
83
+ }
84
+ }
85
+ return result.sort((a, b) => b.count - a.count);
86
+ }
87
+ /** Reset all tracked patterns. */
88
+ reset() {
89
+ this.groups.clear();
90
+ }
91
+ /** Serialize for snapshot persistence. */
92
+ serialize() {
93
+ const clusters = [];
94
+ for (const group of this.groups.values()) {
95
+ for (const cluster of group) {
96
+ clusters.push({
97
+ tokens: [...cluster.tokens],
98
+ mask: [...cluster.mask],
99
+ count: cluster.count,
100
+ examples: [...cluster.examples],
101
+ });
102
+ }
103
+ }
104
+ return { clusters };
105
+ }
106
+ /** Restore from serialized state. */
107
+ restore(state) {
108
+ this.groups.clear();
109
+ for (const c of state.clusters) {
110
+ const key = c.tokens.length;
111
+ let group = this.groups.get(key);
112
+ if (!group) {
113
+ group = [];
114
+ this.groups.set(key, group);
115
+ }
116
+ group.push({
117
+ tokens: [...c.tokens],
118
+ mask: [...c.mask],
119
+ count: c.count,
120
+ examples: [...c.examples],
121
+ });
122
+ }
123
+ }
124
+ // ── Private ──
125
+ tokenize(text) {
126
+ return text
127
+ .split(/\s+/)
128
+ .filter((t) => t.length > 0)
129
+ .slice(0, this.maxDepth * 4);
130
+ }
131
+ similarity(tokens, cluster) {
132
+ const len = Math.min(tokens.length, this.maxDepth);
133
+ let matches = 0;
134
+ for (let i = 0; i < len; i++) {
135
+ if (cluster.mask[i] || tokens[i] === cluster.tokens[i]) {
136
+ matches++;
137
+ }
138
+ }
139
+ return matches / len;
140
+ }
141
+ buildTemplate(cluster) {
142
+ return cluster.tokens.map((tok, i) => (cluster.mask[i] ? '<*>' : tok)).join(' ');
143
+ }
144
+ totalClusters() {
145
+ let total = 0;
146
+ for (const group of this.groups.values()) {
147
+ total += group.length;
148
+ }
149
+ return total;
150
+ }
151
+ evictSmallest() {
152
+ let minCount = Infinity;
153
+ let minGroup = null;
154
+ let minIndex = -1;
155
+ for (const group of this.groups.values()) {
156
+ for (let i = 0; i < group.length; i++) {
157
+ if (group[i].count < minCount) {
158
+ minCount = group[i].count;
159
+ minGroup = group;
160
+ minIndex = i;
161
+ }
162
+ }
163
+ }
164
+ if (minGroup && minIndex >= 0) {
165
+ minGroup.splice(minIndex, 1);
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,31 @@
1
+ const PROVENANCE_LABELS = {
2
+ reported: 'provider-reported',
3
+ estimated: 'estimated from catalog pricing',
4
+ mixed: 'partly provider-reported, partly estimated',
5
+ unpriced: 'unpriced',
6
+ none: '',
7
+ };
8
+ /**
9
+ * Human-readable qualifier for an aggregate cost, for report footers and
10
+ * table cells. Empty when there is nothing to qualify.
11
+ */
12
+ export function describeCostProvenance(tokens) {
13
+ const base = PROVENANCE_LABELS[tokens.costProvenance] ?? '';
14
+ const unpriced = tokens.unpricedCalls > 0
15
+ ? `${tokens.unpricedCalls} unpriced call${tokens.unpricedCalls === 1 ? '' : 's'}`
16
+ : '';
17
+ if (base && unpriced)
18
+ return `${base} · ${unpriced}`;
19
+ return base || unpriced;
20
+ }
21
+ /** Classify a cost breakdown by how many events fell into each bucket. */
22
+ export function classifyCostProvenance(counts) {
23
+ const { reportedCalls, estimatedCalls, unpricedCalls } = counts;
24
+ if (reportedCalls > 0 && estimatedCalls > 0)
25
+ return 'mixed';
26
+ if (reportedCalls > 0)
27
+ return 'reported';
28
+ if (estimatedCalls > 0)
29
+ return 'estimated';
30
+ return unpricedCalls > 0 ? 'unpriced' : 'none';
31
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Session snapshot sidecar — persists aggregator state + reader position
3
+ * so that re-attaching to a session skips full replay.
4
+ *
5
+ * Snapshots are stored in ~/.config/sidekick/snapshots/{sessionId}.json.
6
+ * They are invalidated when the source file changes beyond the snapshot position.
7
+ *
8
+ * @module aggregation/snapshot
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { getConfigDir } from '../paths';
13
+ import { atomicWriteJsonSync } from '../writers/atomic';
14
+ import { SNAPSHOT_SCHEMA_VERSION } from './EventAggregator';
15
+ /** Current schema version — bump when format changes. */
16
+ const SNAPSHOT_VERSION = SNAPSHOT_SCHEMA_VERSION;
17
+ /** Newest snapshots kept on disk; older ones are pruned opportunistically. */
18
+ export const MAX_SNAPSHOT_FILES = 200;
19
+ /** Prune runs once per this many saves, so steady-state writes stay one file each. */
20
+ const PRUNE_EVERY_N_SAVES = 25;
21
+ let savesSincePrune = 0;
22
+ /** Returns the directory for snapshot files. */
23
+ function getSnapshotsDir() {
24
+ return path.join(getConfigDir(), 'snapshots');
25
+ }
26
+ /** Returns the path to a snapshot file for a given session. */
27
+ export function getSnapshotPath(sessionId) {
28
+ // Sanitize sessionId for filesystem safety (remove path separators)
29
+ const safe = sessionId.replace(/[/\\:]/g, '_');
30
+ return path.join(getSnapshotsDir(), `${safe}.json`);
31
+ }
32
+ /**
33
+ * Saves a session snapshot to disk.
34
+ *
35
+ * Creates the snapshots directory if it doesn't exist. Uses the shared
36
+ * fsynced atomic writer with a per-process temp name, so the CLI dashboard and
37
+ * the extension host snapshotting the same session cannot collide on a shared
38
+ * `.tmp` file. Every {@link PRUNE_EVERY_N_SAVES} saves the directory is
39
+ * trimmed to the newest {@link MAX_SNAPSHOT_FILES}; snapshots are caches, so
40
+ * dropping an old one only costs a replay.
41
+ */
42
+ export function saveSnapshot(snapshot) {
43
+ try {
44
+ const dir = getSnapshotsDir();
45
+ if (!fs.existsSync(dir)) {
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ }
48
+ atomicWriteJsonSync(getSnapshotPath(snapshot.sessionId), snapshot);
49
+ savesSincePrune += 1;
50
+ if (savesSincePrune >= PRUNE_EVERY_N_SAVES) {
51
+ savesSincePrune = 0;
52
+ pruneSnapshots(MAX_SNAPSHOT_FILES);
53
+ }
54
+ }
55
+ catch {
56
+ // Non-critical — snapshot failure should never break session monitoring
57
+ }
58
+ }
59
+ /**
60
+ * Deletes all but the newest `keep` snapshot files (by modification time).
61
+ * Returns how many files were removed. Best-effort; never throws.
62
+ */
63
+ export function pruneSnapshots(keep = MAX_SNAPSHOT_FILES) {
64
+ try {
65
+ const dir = getSnapshotsDir();
66
+ const entries = fs
67
+ .readdirSync(dir)
68
+ .filter((name) => name.endsWith('.json'))
69
+ .map((name) => {
70
+ const filePath = path.join(dir, name);
71
+ try {
72
+ return { filePath, mtimeMs: fs.statSync(filePath).mtimeMs };
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ })
78
+ .filter((entry) => entry !== null)
79
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
80
+ let removed = 0;
81
+ for (const entry of entries.slice(Math.max(0, keep))) {
82
+ try {
83
+ fs.unlinkSync(entry.filePath);
84
+ removed += 1;
85
+ }
86
+ catch {
87
+ // Already gone or locked; skip.
88
+ }
89
+ }
90
+ return removed;
91
+ }
92
+ catch {
93
+ return 0;
94
+ }
95
+ }
96
+ /**
97
+ * Loads a session snapshot from disk if it exists and is valid.
98
+ *
99
+ * Returns null if:
100
+ * - No snapshot file exists
101
+ * - Schema version doesn't match
102
+ * - JSON parse fails
103
+ *
104
+ * The caller is responsible for staleness checks (comparing sourceSize
105
+ * and readerPosition against the actual file).
106
+ */
107
+ export function loadSnapshot(sessionId) {
108
+ try {
109
+ const filePath = getSnapshotPath(sessionId);
110
+ if (!fs.existsSync(filePath)) {
111
+ return null;
112
+ }
113
+ const raw = fs.readFileSync(filePath, 'utf-8');
114
+ const snapshot = JSON.parse(raw);
115
+ // Version check
116
+ if (snapshot.version !== SNAPSHOT_VERSION) {
117
+ deleteSnapshot(sessionId);
118
+ return null;
119
+ }
120
+ return snapshot;
121
+ }
122
+ catch {
123
+ // Corrupt or unreadable — delete and move on
124
+ deleteSnapshot(sessionId);
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Deletes a session snapshot file.
130
+ */
131
+ export function deleteSnapshot(sessionId) {
132
+ try {
133
+ const filePath = getSnapshotPath(sessionId);
134
+ if (fs.existsSync(filePath)) {
135
+ fs.unlinkSync(filePath);
136
+ }
137
+ }
138
+ catch {
139
+ // Best-effort cleanup
140
+ }
141
+ }
142
+ /**
143
+ * Validates whether a snapshot is still usable for a given source file.
144
+ *
145
+ * For JSONL providers: checks that the file hasn't been truncated
146
+ * (current size >= snapshot position) and that the size matches or has grown.
147
+ *
148
+ * For DB-backed providers: sourceSize is 0, so we always consider valid
149
+ * (the DB reader's timestamp-based cursor handles staleness naturally).
150
+ */
151
+ export function isSnapshotValid(snapshot, currentSourceSize) {
152
+ // DB-backed providers store sourceSize=0 — always valid (cursor is a timestamp)
153
+ if (snapshot.sourceSize === 0) {
154
+ return true;
155
+ }
156
+ // File was truncated or rewritten (smaller than when we snapshotted)
157
+ if (currentSourceSize < snapshot.readerPosition) {
158
+ return false;
159
+ }
160
+ return true;
161
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Aggregated metrics output types for EventAggregator.
3
+ * Re-exports canonical types from sessionEvent and defines aggregation-specific shapes.
4
+ */
5
+ export {};
@@ -0,0 +1,18 @@
1
+ export function calculateCodeImpact(totalCost, additions, deletions, models = []) {
2
+ const safeAdditions = Math.max(0, additions);
3
+ const safeDeletions = Math.max(0, deletions);
4
+ const changedLines = safeAdditions + safeDeletions;
5
+ return {
6
+ changedLines,
7
+ additions: safeAdditions,
8
+ deletions: safeDeletions,
9
+ totalCost,
10
+ costPerChangedLine: changedLines > 0 ? totalCost / changedLines : null,
11
+ byModel: models.map((model) => ({
12
+ ...model,
13
+ priced: model.priced !== false,
14
+ changedLines,
15
+ costPerChangedLine: changedLines > 0 && model.priced !== false ? model.cost / changedLines : null,
16
+ })),
17
+ };
18
+ }
@@ -0,0 +1,37 @@
1
+ import { calculateNormalizedUsageCost, normalizeProviderUsage } from '../usageNormalization';
2
+ export function calculateCompactionLedger(events, model) {
3
+ const tokensEvicted = events.reduce((sum, event) => sum + Math.max(0, event.tokensReclaimed), 0);
4
+ const reportedCount = events.filter((event) => event.source === 'reported').length;
5
+ const heuristicCount = events.length - reportedCount;
6
+ // Re-establishing evicted context means sending it again as uncached input.
7
+ // Price it through the same normalized path every other cost figure uses so
8
+ // the ledger can never disagree with the session total on the same tokens.
9
+ const reestablishment = model
10
+ ? calculateNormalizedUsageCost({
11
+ usage: normalizeProviderUsage({
12
+ semantics: 'sidekick',
13
+ inputTokens: tokensEvicted,
14
+ reasoningIncludedInOutput: true,
15
+ }),
16
+ modelId: model,
17
+ })
18
+ : null;
19
+ return {
20
+ count: events.length,
21
+ tokensEvicted,
22
+ reestablishmentCostUsd: reestablishment?.costUsd ?? null,
23
+ source: reportedCount > 0 && heuristicCount > 0
24
+ ? 'mixed'
25
+ : reportedCount > 0
26
+ ? 'reported'
27
+ : 'heuristic',
28
+ reportedCount,
29
+ heuristicCount,
30
+ };
31
+ }
32
+ export function formatCompactionLedger(ledger) {
33
+ const cost = ledger.reestablishmentCostUsd == null
34
+ ? 'cost unavailable'
35
+ : `~$${ledger.reestablishmentCostUsd.toFixed(2)} re-establishing context`;
36
+ return `${ledger.count} compaction${ledger.count === 1 ? '' : 's'} · ${ledger.tokensEvicted.toLocaleString()} tokens evicted · ${cost} · ${ledger.source}`;
37
+ }