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,450 @@
1
+ import { resolveActiveClaudeAccount } from './accounts';
2
+ import { CodexQuotaWatcher } from './codexQuotaWatcher';
3
+ import { readClaudeMaxCredentials } from './credentials';
4
+ import { fetchPeakHoursStatus } from './peakHours';
5
+ import { fetchQuota } from './quota';
6
+ import { describeQuotaFailure } from './quotaPresentation';
7
+ import { onAccountsChanged } from './accountChanges';
8
+ const ACTIVE_POLL_INTERVAL_MS = 60_000;
9
+ const IDLE_POLL_INTERVAL_MS = 300_000;
10
+ const TRANSIENT_FAILURE_BACKOFF_MS = [30_000, 60_000, 120_000, 240_000, 300_000];
11
+ const PEAK_HOURS_CACHE_MAX_AGE_MS = 10 * 60 * 1000;
12
+ const SIGNED_OUT_ERROR = 'Sign in to a Claude account to view quota';
13
+ function makeUnavailableClaudeState(error, failureKind) {
14
+ return {
15
+ runtimeProvider: 'claude',
16
+ fiveHour: { utilization: 0, resetsAt: '' },
17
+ sevenDay: { utilization: 0, resetsAt: '' },
18
+ available: false,
19
+ error,
20
+ failureKind,
21
+ providerId: 'claude-code',
22
+ source: 'api',
23
+ };
24
+ }
25
+ function formatDelay(ms) {
26
+ if (ms % 60_000 === 0)
27
+ return `${ms / 60_000}m`;
28
+ if (ms % 1_000 === 0)
29
+ return `${ms / 1_000}s`;
30
+ return `${ms}ms`;
31
+ }
32
+ function unavailablePeakHoursState() {
33
+ return {
34
+ status: 'unknown',
35
+ isPeak: false,
36
+ sessionLimitSpeed: 'unknown',
37
+ label: 'Peak hours unavailable',
38
+ peakHoursDescription: '',
39
+ nextChange: null,
40
+ minutesUntilChange: null,
41
+ note: '',
42
+ updatedAt: new Date().toISOString(),
43
+ unavailable: true,
44
+ };
45
+ }
46
+ export class MultiProviderQuotaService {
47
+ activeIntervalMs;
48
+ idleIntervalMs;
49
+ transientFailureBackoffMs;
50
+ peakHoursCacheMaxAgeMs;
51
+ includePeakHours;
52
+ readClaudeCredentials;
53
+ readClaudeAccount;
54
+ fetchClaudeQuota;
55
+ fetchPeakHours;
56
+ log;
57
+ subscribeAccountsChanged;
58
+ listeners = [];
59
+ codexWatcher;
60
+ ownsCodexWatcher;
61
+ codexSubscription = null;
62
+ zaiWatcher;
63
+ ownsZaiWatcher;
64
+ zaiSubscription = null;
65
+ timer;
66
+ polling = false;
67
+ quotaByProvider = {};
68
+ lastEmittedClaudeState = null;
69
+ lastSuccessfulClaudeState = null;
70
+ pollingMode = 'idle';
71
+ transientFailureStreak = 0;
72
+ cachedFallbackActive = false;
73
+ authUnavailableActive = false;
74
+ currentPeakHours;
75
+ lastFreshPeakHoursAt = 0;
76
+ peakHoursErrorLogged = false;
77
+ claudeDormant = false;
78
+ accountSubscription = null;
79
+ pollInFlight = false;
80
+ pendingWake = false;
81
+ constructor(options = {}) {
82
+ this.activeIntervalMs = options.activeIntervalMs ?? ACTIVE_POLL_INTERVAL_MS;
83
+ this.idleIntervalMs = options.idleIntervalMs ?? IDLE_POLL_INTERVAL_MS;
84
+ this.transientFailureBackoffMs =
85
+ options.transientFailureBackoffMs ?? TRANSIENT_FAILURE_BACKOFF_MS;
86
+ this.peakHoursCacheMaxAgeMs = options.peakHoursCacheMaxAgeMs ?? PEAK_HOURS_CACHE_MAX_AGE_MS;
87
+ this.includePeakHours = options.includePeakHours ?? true;
88
+ this.readClaudeCredentials = options.readClaudeCredentials ?? readClaudeMaxCredentials;
89
+ this.readClaudeAccount = options.readClaudeAccount ?? resolveActiveClaudeAccount;
90
+ this.fetchClaudeQuota = options.fetchClaudeQuota ?? fetchQuota;
91
+ this.fetchPeakHours = options.fetchPeakHours ?? fetchPeakHoursStatus;
92
+ this.subscribeAccountsChanged = options.subscribeAccountsChanged ?? onAccountsChanged;
93
+ this.log = options.log;
94
+ if (options.codexWatcher !== undefined) {
95
+ this.codexWatcher = options.codexWatcher;
96
+ this.ownsCodexWatcher = false;
97
+ }
98
+ else if (options.codexWorkspacePath) {
99
+ this.codexWatcher = new CodexQuotaWatcher(options.codexWorkspacePath);
100
+ this.ownsCodexWatcher = true;
101
+ }
102
+ else {
103
+ this.codexWatcher = null;
104
+ this.ownsCodexWatcher = false;
105
+ }
106
+ this.codexSubscription =
107
+ this.codexWatcher?.onUpdate((state) => {
108
+ this.updateProviderQuota('codex', state);
109
+ }) ?? null;
110
+ if (options.zaiWatcher !== undefined) {
111
+ this.zaiWatcher = options.zaiWatcher;
112
+ this.ownsZaiWatcher = false;
113
+ }
114
+ else {
115
+ this.zaiWatcher = null;
116
+ this.ownsZaiWatcher = false;
117
+ }
118
+ this.zaiSubscription =
119
+ this.zaiWatcher?.onUpdate((state) => {
120
+ this.updateProviderQuota('zai', state);
121
+ }) ?? null;
122
+ }
123
+ startPolling() {
124
+ if (this.polling)
125
+ return;
126
+ this.polling = true;
127
+ this.accountSubscription ??= this.subscribeAccountsChanged(() => this.wakeForAccountChange());
128
+ this.codexWatcher?.start();
129
+ this.zaiWatcher?.start();
130
+ this.wakeForAccountChange();
131
+ this.log?.('[Quota] Polling started');
132
+ }
133
+ stopPolling() {
134
+ if (!this.polling)
135
+ return;
136
+ this.polling = false;
137
+ this.claudeDormant = false;
138
+ if (this.timer) {
139
+ clearTimeout(this.timer);
140
+ this.timer = undefined;
141
+ }
142
+ this.codexWatcher?.stop();
143
+ this.zaiWatcher?.stop();
144
+ this.accountSubscription?.dispose();
145
+ this.accountSubscription = null;
146
+ this.log?.('[Quota] Polling stopped');
147
+ }
148
+ setPollingMode(mode) {
149
+ if (this.pollingMode === mode)
150
+ return;
151
+ this.pollingMode = mode;
152
+ if (!this.polling)
153
+ return;
154
+ if (this.transientFailureStreak > 0 && this.timer)
155
+ return;
156
+ if (this.timer) {
157
+ clearTimeout(this.timer);
158
+ this.timer = undefined;
159
+ }
160
+ this.scheduleNextPoll();
161
+ }
162
+ onUpdate(cb) {
163
+ this.listeners.push(cb);
164
+ return {
165
+ dispose: () => {
166
+ const index = this.listeners.indexOf(cb);
167
+ if (index >= 0)
168
+ this.listeners.splice(index, 1);
169
+ },
170
+ };
171
+ }
172
+ onQuotaUpdate(cb) {
173
+ return this.onUpdate(cb);
174
+ }
175
+ getLatest() {
176
+ return { ...this.quotaByProvider };
177
+ }
178
+ updateProviderQuota(provider, state) {
179
+ if (provider === 'claude') {
180
+ const nextState = this.withClaudeAccountDetails({
181
+ ...state,
182
+ runtimeProvider: 'claude',
183
+ providerId: 'claude-code',
184
+ });
185
+ this.lastEmittedClaudeState = nextState;
186
+ if (nextState.available && nextState.stale !== true) {
187
+ this.lastSuccessfulClaudeState = nextState;
188
+ }
189
+ this.quotaByProvider = {
190
+ ...this.quotaByProvider,
191
+ claude: nextState,
192
+ };
193
+ this.emit();
194
+ return;
195
+ }
196
+ if (provider === 'zai') {
197
+ const nextState = {
198
+ ...state,
199
+ runtimeProvider: 'zai',
200
+ providerId: 'zai',
201
+ };
202
+ this.quotaByProvider = {
203
+ ...this.quotaByProvider,
204
+ zai: nextState,
205
+ };
206
+ this.emit();
207
+ return;
208
+ }
209
+ const nextState = {
210
+ ...state,
211
+ runtimeProvider: 'codex',
212
+ providerId: 'codex',
213
+ };
214
+ this.quotaByProvider = {
215
+ ...this.quotaByProvider,
216
+ codex: nextState,
217
+ };
218
+ this.emit();
219
+ }
220
+ dispose() {
221
+ this.stopPolling();
222
+ this.codexSubscription?.dispose();
223
+ this.codexSubscription = null;
224
+ if (this.ownsCodexWatcher) {
225
+ this.codexWatcher?.dispose();
226
+ }
227
+ this.zaiSubscription?.dispose();
228
+ this.zaiSubscription = null;
229
+ if (this.ownsZaiWatcher) {
230
+ this.zaiWatcher?.dispose();
231
+ }
232
+ this.listeners.splice(0, this.listeners.length);
233
+ }
234
+ async refreshPeakHours() {
235
+ if (!this.includePeakHours)
236
+ return;
237
+ let result;
238
+ try {
239
+ result = await this.fetchPeakHours();
240
+ }
241
+ catch (error) {
242
+ if (!this.peakHoursErrorLogged) {
243
+ this.log?.('[Quota] Peak-hours fetch threw unexpectedly', error);
244
+ this.peakHoursErrorLogged = true;
245
+ }
246
+ result = unavailablePeakHoursState();
247
+ }
248
+ if (!result.unavailable) {
249
+ this.currentPeakHours = result;
250
+ this.lastFreshPeakHoursAt = Date.now();
251
+ this.peakHoursErrorLogged = false;
252
+ return;
253
+ }
254
+ const cachedIsFresh = this.currentPeakHours !== undefined &&
255
+ !this.currentPeakHours.unavailable &&
256
+ Date.now() - this.lastFreshPeakHoursAt < this.peakHoursCacheMaxAgeMs;
257
+ if (cachedIsFresh)
258
+ return;
259
+ this.currentPeakHours = result;
260
+ }
261
+ async poll() {
262
+ if (!this.polling || this.pollInFlight) {
263
+ this.pendingWake = this.polling;
264
+ return;
265
+ }
266
+ this.pollInFlight = true;
267
+ this.pendingWake = false;
268
+ try {
269
+ let accountPresent = false;
270
+ try {
271
+ accountPresent = this.readClaudeAccount().source !== 'none';
272
+ }
273
+ catch {
274
+ accountPresent = false;
275
+ }
276
+ if (!accountPresent) {
277
+ this.claudeDormant = true;
278
+ this.resetTransientFailureState();
279
+ this.authUnavailableActive = false;
280
+ this.lastSuccessfulClaudeState = null;
281
+ // Stay visible while dormant: consumers render their sign-in prompt
282
+ // from an unavailable auth entry, not from a missing map key.
283
+ if (this.quotaByProvider.claude?.error !== SIGNED_OUT_ERROR) {
284
+ this.lastEmittedClaudeState = null;
285
+ this.emitClaudeState(makeUnavailableClaudeState(SIGNED_OUT_ERROR, 'auth'));
286
+ }
287
+ return;
288
+ }
289
+ this.claudeDormant = false;
290
+ await this.refreshPeakHours();
291
+ const creds = await this.readClaudeCredentials();
292
+ if (!creds) {
293
+ this.handleUnavailableState(makeUnavailableClaudeState('Renew your Claude Max OAuth to view quota', 'auth'));
294
+ return;
295
+ }
296
+ const state = await this.fetchClaudeQuota(creds.accessToken);
297
+ if (!state.available) {
298
+ this.handleUnavailableState({
299
+ ...state,
300
+ runtimeProvider: 'claude',
301
+ providerId: 'claude-code',
302
+ source: state.source ?? 'api',
303
+ });
304
+ return;
305
+ }
306
+ const recoveredAttempts = this.transientFailureStreak;
307
+ const recoveredFromFallback = recoveredAttempts > 0;
308
+ this.resetTransientFailureState();
309
+ this.authUnavailableActive = false;
310
+ if (recoveredFromFallback) {
311
+ this.log?.(`[Quota] Fetch recovered after ${recoveredAttempts} failed attempt(s)`);
312
+ }
313
+ this.emitClaudeState({
314
+ ...state,
315
+ runtimeProvider: 'claude',
316
+ providerId: 'claude-code',
317
+ source: state.source ?? 'api',
318
+ });
319
+ }
320
+ catch (error) {
321
+ if (this.transientFailureStreak === 0) {
322
+ this.log?.('[Quota] Poll error', error);
323
+ }
324
+ const message = error instanceof Error ? error.message : 'Unknown error';
325
+ this.handleTransientFailure(makeUnavailableClaudeState(message));
326
+ }
327
+ finally {
328
+ this.pollInFlight = false;
329
+ if (this.pendingWake) {
330
+ this.pendingWake = false;
331
+ void this.poll();
332
+ }
333
+ else if (!this.claudeDormant) {
334
+ this.scheduleNextPoll();
335
+ }
336
+ }
337
+ }
338
+ wakeForAccountChange() {
339
+ if (!this.polling)
340
+ return;
341
+ if (this.timer) {
342
+ clearTimeout(this.timer);
343
+ this.timer = undefined;
344
+ }
345
+ this.codexWatcher?.start();
346
+ this.zaiWatcher?.start();
347
+ if (this.pollInFlight) {
348
+ this.pendingWake = true;
349
+ return;
350
+ }
351
+ void this.poll();
352
+ }
353
+ emitClaudeState(state) {
354
+ const withPeakHours = this.currentPeakHours !== undefined ? { ...state, peakHours: this.currentPeakHours } : state;
355
+ const withFailure = {
356
+ ...withPeakHours,
357
+ failure: describeQuotaFailure(withPeakHours),
358
+ };
359
+ this.updateProviderQuota('claude', withFailure);
360
+ }
361
+ handleUnavailableState(state) {
362
+ if (state.failureKind === 'auth') {
363
+ this.resetTransientFailureState();
364
+ this.authUnavailableActive = true;
365
+ this.emitClaudeState(state);
366
+ return;
367
+ }
368
+ this.handleTransientFailure(state);
369
+ }
370
+ handleTransientFailure(state) {
371
+ this.authUnavailableActive = false;
372
+ this.transientFailureStreak += 1;
373
+ const retryDelay = this.getTransientFailureDelay();
374
+ const hasVisibleCachedSuccess = this.lastSuccessfulClaudeState !== null && this.lastEmittedClaudeState?.available === true;
375
+ if (this.transientFailureStreak === 1) {
376
+ if (hasVisibleCachedSuccess) {
377
+ this.log?.(`[Quota] Fetch failed, using cached data; retrying in ${formatDelay(retryDelay)}`);
378
+ }
379
+ else {
380
+ this.log?.(`[Quota] Fetch unavailable; retrying in ${formatDelay(retryDelay)}`);
381
+ }
382
+ }
383
+ this.cachedFallbackActive = hasVisibleCachedSuccess;
384
+ if (hasVisibleCachedSuccess) {
385
+ this.emitClaudeState({
386
+ ...this.lastSuccessfulClaudeState,
387
+ error: state.error,
388
+ failureKind: state.failureKind,
389
+ httpStatus: state.httpStatus,
390
+ retryAfterMs: state.retryAfterMs,
391
+ source: 'cache',
392
+ stale: true,
393
+ });
394
+ }
395
+ else {
396
+ this.emitClaudeState(state);
397
+ }
398
+ }
399
+ getTransientFailureDelay() {
400
+ return this.transientFailureBackoffMs[Math.min(this.transientFailureStreak - 1, this.transientFailureBackoffMs.length - 1)];
401
+ }
402
+ resetTransientFailureState() {
403
+ this.transientFailureStreak = 0;
404
+ this.cachedFallbackActive = false;
405
+ }
406
+ scheduleNextPoll() {
407
+ if (!this.polling)
408
+ return;
409
+ if (this.timer) {
410
+ clearTimeout(this.timer);
411
+ }
412
+ let delay = this.pollingMode === 'active' ? this.activeIntervalMs : this.idleIntervalMs;
413
+ if (this.transientFailureStreak > 0) {
414
+ delay = this.getTransientFailureDelay();
415
+ }
416
+ else if (this.authUnavailableActive || this.lastEmittedClaudeState?.available !== true) {
417
+ delay = this.idleIntervalMs;
418
+ }
419
+ else if (this.cachedFallbackActive) {
420
+ delay = this.idleIntervalMs;
421
+ }
422
+ this.timer = setTimeout(() => void this.poll(), delay);
423
+ }
424
+ withClaudeAccountDetails(state) {
425
+ const resolved = this.readClaudeAccount();
426
+ const accountEmail = resolved.email;
427
+ if (!accountEmail) {
428
+ const nextState = { ...state };
429
+ delete nextState.accountLabel;
430
+ delete nextState.accountDetail;
431
+ return nextState;
432
+ }
433
+ return {
434
+ ...state,
435
+ accountLabel: resolved.label ?? accountEmail,
436
+ accountDetail: accountEmail,
437
+ };
438
+ }
439
+ emit() {
440
+ const snapshot = { ...this.quotaByProvider };
441
+ for (const listener of this.listeners) {
442
+ try {
443
+ listener(snapshot);
444
+ }
445
+ catch {
446
+ // Listener failures should not break polling.
447
+ }
448
+ }
449
+ }
450
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Node-only entry.
3
+ *
4
+ * Pricing catalog hydration from the LiteLLM source. Requires `node:fs`
5
+ * and `node:path`; safe for extension-host and CLI consumers. Not safe
6
+ * for browser bundles — use `sidekick-shared/browser` there.
7
+ */
8
+ export { hydratePricingCatalog, normalizeLiteLlmCatalog, normalizeLiteLlmContextWindows, LITELLM_CATALOG_URL, } from './pricingCatalog';
9
+ export { loadObservedContextWindows, recordObservedContextWindow, getObservedContextWindowPath, } from './observedContextWindows';
10
+ export { listRecentSessions, readSessionTranscript } from './sessionTranscripts';
11
+ export { collectPromptHistory, isPromptHistorySessionSettled, parsePromptHistoryCursor, } from './promptHistory';
12
+ export { collectSessionPromptHistory } from './sessionPromptHistory';
13
+ export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
14
+ export { computeSessionFileStats, firstUserPrompt, providerContextSizeFn, readSessionFileStats, } from './sessionStats';
15
+ export { readSessionReportInputs } from './report/sessionReportInputs';
16
+ export { fingerprintString, sessionFingerprintParts } from './sessionFingerprint';
17
+ export { STATE_FILE_NAME, STATE_FILE_SCHEMA_VERSION, billingBlockToStateFile, getStateFilePath, quotaToStateFile, readStateFile, writeStateFile, } from './stateFile';
18
+ export { BILLING_BLOCK_DURATION_MS, computeBillingBlocks, findActiveBillingBlock, } from './usage/billingBlocks';
19
+ export { MAX_USAGE_CACHE_FILES, USAGE_CACHE_VERSION, collectUsageEvents, getUsageCacheDir, pruneUsageCache, } from './usage/usageEvents';
20
+ export { collectSessionTokenTotals, combineSessionTokenTotals, readMainThreadTokenTotals, scanSessionSubagents, subagentTokenTotals, } from './sessionTokenTotals';
21
+ export { applySessionSummary, isFileImported, markFileImported, removeSessionSummary, sessionSummaryFromStats, } from './historicalStore';
22
+ export { ACTIVE_SESSION_MTIME_THRESHOLD_MS, importSessionHistory, } from './usage/importSessionHistory';
23
+ export { bucketUsage, summarizeUsageRows, usageBucketKey, weekKey } from './usage/usageReports';
24
+ export { findCodexRolloutFile } from './providers/codex';
25
+ export { walkRolloutFiles, walkRolloutFilesAsync } from './providers/rolloutWalker';
26
+ export { readCodexHistory } from './parsers/codexHistory';
27
+ export { listSessionPreviews, listSessionPreviewsAsync, readSessionPreview, readSessionPreviewAsync, } from './sessionPreviews';
28
+ export { ObservedSessionCollector, observedSessionSourceFromProvider, fileFingerprint, fileFingerprintParts, } from './observedSessionCollector';
29
+ export { DirectoryListingCache, splitWatchedPath } from './providers/directoryListingCache';
30
+ export { createSessionProviders } from './providers/factory';
31
+ export { SYNTHETIC_MODEL } from './usage/claudeUsageDedupe';
32
+ // Sidekick's config root
33
+ export { CONFIG_DIR_ENV_VAR, getConfigDir, getConfigDirOverride, getDefaultConfigDir, } from './paths';
34
+ // Claude Code session directories (Claude's own path encoding)
35
+ export { encodeWorkspacePath as encodeClaudeWorkspacePath, getSessionDirectory as getClaudeSessionDirectory, } from './parsers/sessionPathResolver';
36
+ // Provider homes the CLIs read for this process
37
+ export { getLiveClaudeHome, getDefaultClaudeHome, isDefaultClaudeHome, getClaudeProjectsDir, } from './claudePaths';
38
+ export { getSystemCodexHome, getCodexMonitoringHomes, resolveSidekickCodexHome, getCodexExecutionEnv, getCodexSessionsDirs, CODEX_SESSIONS_SUBDIR, CODEX_ARCHIVED_SESSIONS_SUBDIR, } from './codexPaths';
39
+ export { fetchProviderServiceStatus } from './providerServiceStatus';
40
+ // Tool categories across providers
41
+ export { classifyTool, TOOL_CATEGORIES } from './toolCategory';
42
+ // Workspace roots and git worktrees
43
+ export { resolveWorkspaceRoots } from './workspaceRoots';
44
+ export { discoverWorktreeSiblings, resolveWorktreeMainRepo } from './parsers/sessionPathResolver';
45
+ // Bounded JSONL tail reader
46
+ export { createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, } from './watchers/jsonlTail';
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Persisted record of context windows providers actually reported (Node-only).
3
+ *
4
+ * Some agents report the real context window for the running session — Codex
5
+ * emits `model_context_window` on every `token_count` event. That number is the
6
+ * *effective* window for the account and tier, which can sit well below the
7
+ * model's published maximum (e.g. 258,400 observed for `gpt-5.6-sol` against a
8
+ * catalog maximum of 1,050,000).
9
+ *
10
+ * A live session uses that value directly. This module keeps it across runs, so
11
+ * historical and static views don't fall back to a published maximum that would
12
+ * make the context gauge read far too empty.
13
+ *
14
+ * Precedence, most-trusted first: runtime → observed (here) → LiteLLM catalog →
15
+ * static table. See `modelContext.ts`.
16
+ *
17
+ * ## Where the store lives — two seams, one story
18
+ *
19
+ * Reader ({@link loadObservedContextWindows}) and writer
20
+ * ({@link recordObservedContextWindow}) resolve the store path identically:
21
+ * explicit `cacheDir` option → `setConfigDir()` / `SIDEKICK_CONFIG_DIR` (via
22
+ * `getConfigDir()`) → platform default. The global config-dir override is the
23
+ * deployment-level seam external consumers use to relocate every Sidekick
24
+ * store at once; the per-call `cacheDir` is the isolation seam for tests and
25
+ * multi-tenant hosts. Providers deliberately do NOT thread `cacheDir` —
26
+ * `CodexProvider` records observations with default options, which follow the
27
+ * global override, so a default-options load always sees what a
28
+ * default-options record wrote.
29
+ *
30
+ * @module observedContextWindows
31
+ */
32
+ import { promises as fs } from 'node:fs';
33
+ import * as path from 'node:path';
34
+ import { _setObservedContextWindows, _getObservedContextWindows } from './modelContext';
35
+ import { getConfigDir } from './paths';
36
+ import { updateJsonStoreAtomic } from './writers/atomic';
37
+ // ── Constants ──
38
+ const STORE_FILE_NAME = 'observed-context-windows.json';
39
+ const STORE_VERSION = 1;
40
+ /**
41
+ * Observations older than this are ignored on load. A provider reports its
42
+ * context window on every session, so a live model refreshes its entry
43
+ * constantly; an entry that has gone quiet this long belongs to a tier or
44
+ * model the user no longer runs and must not outrank the catalog forever.
45
+ */
46
+ export const OBSERVED_CONTEXT_WINDOW_TTL_MS = 30 * 86_400_000;
47
+ /** Resolve the on-disk store path. */
48
+ export function getObservedContextWindowPath(cacheDir) {
49
+ return path.join(cacheDir ?? getConfigDir(), STORE_FILE_NAME);
50
+ }
51
+ function emptyStore() {
52
+ return { version: STORE_VERSION, models: {} };
53
+ }
54
+ /**
55
+ * Last value successfully persisted, per resolved store path.
56
+ *
57
+ * The in-memory override table in `modelContext` is process-global by design —
58
+ * a lookup should answer the same way everywhere. That makes it unusable as the
59
+ * write dedupe: with two `cacheDir`s in one process (two temp dirs in a test
60
+ * suite, a multi-tenant host, a CLI and extension host sharing a process), the
61
+ * global would already hold the value from the first store and the second store
62
+ * would never be written at all.
63
+ */
64
+ const lastPersistedByStore = new Map();
65
+ /** Reset the per-store write dedupe. Test-only; not part of the public API. */
66
+ export function _clearObservedContextWindowWriteCache() {
67
+ lastPersistedByStore.clear();
68
+ }
69
+ /**
70
+ * Keep only well-formed, recent entries; a hand-edited or partly-corrupt file
71
+ * shouldn't poison lookups, and a stale observation shouldn't outrank the
72
+ * catalog (see {@link OBSERVED_CONTEXT_WINDOW_TTL_MS}).
73
+ */
74
+ function toOverrideMap(store, now = Date.now()) {
75
+ const out = {};
76
+ for (const [modelId, entry] of Object.entries(store.models ?? {})) {
77
+ const window = entry?.contextWindow;
78
+ if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
79
+ continue;
80
+ const observedAt = typeof entry.observedAt === 'string' ? Date.parse(entry.observedAt) : NaN;
81
+ if (Number.isFinite(observedAt) && now - observedAt > OBSERVED_CONTEXT_WINDOW_TTL_MS)
82
+ continue;
83
+ out[modelId] = window;
84
+ }
85
+ return out;
86
+ }
87
+ // ── Public API ──
88
+ /**
89
+ * Load persisted observations and apply them as context-window overrides.
90
+ *
91
+ * Always resolves — never throws. A missing, unreadable, or malformed store
92
+ * leaves the overrides empty and lookups fall through to catalog/static.
93
+ */
94
+ export async function loadObservedContextWindows(options = {}) {
95
+ const storePath = getObservedContextWindowPath(options.cacheDir);
96
+ try {
97
+ const raw = await fs.readFile(storePath, 'utf8');
98
+ const parsed = JSON.parse(raw);
99
+ if (!parsed || typeof parsed !== 'object' || !parsed.models)
100
+ return {};
101
+ const overrides = toOverrideMap(parsed);
102
+ _setObservedContextWindows(overrides);
103
+ return overrides;
104
+ }
105
+ catch (err) {
106
+ const code = err.code;
107
+ if (code !== 'ENOENT') {
108
+ options.logger?.(`observedContextWindows: failed to load ${storePath}: ${String(err)}`);
109
+ }
110
+ return {};
111
+ }
112
+ }
113
+ /**
114
+ * Record a provider-reported context window for a model.
115
+ *
116
+ * Applies in memory immediately, then persists — but only when the value
117
+ * actually changed *for that store*. Codex reports a window on every
118
+ * `token_count` event, so an unconditional write would mean disk I/O many times
119
+ * per session for a value that almost never moves.
120
+ *
121
+ * Always resolves — never throws. Persistence failures are logged and dropped;
122
+ * the in-memory override still stands for this process, and the next call
123
+ * retries the write.
124
+ */
125
+ export async function recordObservedContextWindow(modelId, contextWindow, options = {}) {
126
+ const normalizedId = modelId?.trim().toLowerCase();
127
+ if (!normalizedId)
128
+ return;
129
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0)
130
+ return;
131
+ const current = _getObservedContextWindows();
132
+ if (current[normalizedId] !== contextWindow) {
133
+ _setObservedContextWindows({ ...current, [normalizedId]: contextWindow });
134
+ }
135
+ const storePath = getObservedContextWindowPath(options.cacheDir);
136
+ const persisted = lastPersistedByStore.get(storePath);
137
+ if (persisted?.get(normalizedId) === contextWindow)
138
+ return;
139
+ try {
140
+ // Lock-guarded read-modify-write: the CLI and the extension host can both
141
+ // be observing sessions at once, and each should merge rather than clobber.
142
+ await updateJsonStoreAtomic(storePath, emptyStore, (latest) => {
143
+ const models = latest?.models ?? {};
144
+ if (models[normalizedId]?.contextWindow === contextWindow)
145
+ return latest;
146
+ return {
147
+ version: STORE_VERSION,
148
+ models: {
149
+ ...models,
150
+ [normalizedId]: { contextWindow, observedAt: new Date().toISOString() },
151
+ },
152
+ };
153
+ });
154
+ // Recorded only on success, so a transient EACCES retries next time.
155
+ let store = lastPersistedByStore.get(storePath);
156
+ if (!store) {
157
+ store = new Map();
158
+ lastPersistedByStore.set(storePath, store);
159
+ }
160
+ store.set(normalizedId, contextWindow);
161
+ }
162
+ catch (err) {
163
+ options.logger?.(`observedContextWindows: failed to persist ${storePath}: ${String(err)}`);
164
+ }
165
+ }