sidekick-shared 0.24.5 → 0.26.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 (157) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +205 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +170 -0
  7. package/dist/accountRegistry.d.ts +25 -2
  8. package/dist/accountRegistry.js +41 -13
  9. package/dist/accountStatus.d.ts +9 -1
  10. package/dist/accountStatus.js +3 -3
  11. package/dist/accounts.d.ts +3 -1
  12. package/dist/accounts.js +84 -64
  13. package/dist/aggregation/EventAggregator.d.ts +15 -2
  14. package/dist/aggregation/EventAggregator.js +54 -9
  15. package/dist/aggregation/costProvenance.d.ts +14 -0
  16. package/dist/aggregation/costProvenance.js +35 -0
  17. package/dist/aggregation/snapshot.d.ts +13 -2
  18. package/dist/aggregation/snapshot.js +57 -6
  19. package/dist/aggregation/types.d.ts +30 -1
  20. package/dist/analytics/compactionLedger.js +15 -5
  21. package/dist/analytics/qualityScore.js +2 -1
  22. package/dist/browser.d.ts +19 -4
  23. package/dist/browser.js +43 -1
  24. package/dist/codexProfiles.d.ts +3 -1
  25. package/dist/codexProfiles.js +4 -2
  26. package/dist/codexQuota.d.ts +2 -0
  27. package/dist/codexQuota.js +11 -66
  28. package/dist/codexQuotaWatcher.d.ts +6 -0
  29. package/dist/codexQuotaWatcher.js +101 -47
  30. package/dist/failingTools.d.ts +37 -0
  31. package/dist/failingTools.js +70 -0
  32. package/dist/formatters/sessionDump.js +13 -7
  33. package/dist/formatting.d.ts +13 -0
  34. package/dist/formatting.js +35 -0
  35. package/dist/historicalStore.d.ts +47 -0
  36. package/dist/historicalStore.js +312 -0
  37. package/dist/index.d.ts +67 -23
  38. package/dist/index.js +138 -26
  39. package/dist/modelAliases.d.ts +13 -0
  40. package/dist/modelAliases.js +58 -0
  41. package/dist/modelCatalog.d.ts +32 -0
  42. package/dist/modelCatalog.js +145 -0
  43. package/dist/modelContext.d.ts +36 -3
  44. package/dist/modelContext.js +109 -34
  45. package/dist/modelInfo.d.ts +30 -1
  46. package/dist/modelInfo.js +116 -26
  47. package/dist/multiProviderQuotaService.d.ts +8 -0
  48. package/dist/multiProviderQuotaService.js +64 -3
  49. package/dist/node.d.ts +23 -4
  50. package/dist/node.js +50 -1
  51. package/dist/observedContextWindows.d.ts +7 -0
  52. package/dist/observedContextWindows.js +17 -2
  53. package/dist/observedSessionCollector.d.ts +72 -6
  54. package/dist/observedSessionCollector.js +440 -47
  55. package/dist/parsers/codexHistory.d.ts +4 -2
  56. package/dist/parsers/codexHistory.js +9 -2
  57. package/dist/parsers/sessionActivityDetector.d.ts +28 -0
  58. package/dist/parsers/sessionActivityDetector.js +110 -15
  59. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  60. package/dist/parsers/sessionPathResolver.js +13 -10
  61. package/dist/peakHours.d.ts +34 -3
  62. package/dist/peakHours.js +92 -7
  63. package/dist/pricingCatalog.d.ts +7 -0
  64. package/dist/pricingCatalog.js +8 -5
  65. package/dist/providerIds.d.ts +11 -0
  66. package/dist/providerIds.js +9 -0
  67. package/dist/providerQuota.d.ts +3 -1
  68. package/dist/providerQuota.js +3 -0
  69. package/dist/providers/claudeCode.d.ts +10 -1
  70. package/dist/providers/claudeCode.js +211 -131
  71. package/dist/providers/codex.d.ts +20 -1
  72. package/dist/providers/codex.js +308 -244
  73. package/dist/providers/codexDatabase.d.ts +9 -0
  74. package/dist/providers/codexDatabase.js +107 -2
  75. package/dist/providers/detect.d.ts +2 -0
  76. package/dist/providers/detect.js +29 -24
  77. package/dist/providers/diagnostics.d.ts +13 -0
  78. package/dist/providers/diagnostics.js +88 -0
  79. package/dist/providers/factory.d.ts +20 -0
  80. package/dist/providers/factory.js +57 -0
  81. package/dist/providers/openCode.d.ts +22 -1
  82. package/dist/providers/openCode.js +460 -126
  83. package/dist/providers/openCodeDatabase.d.ts +10 -0
  84. package/dist/providers/openCodeDatabase.js +69 -0
  85. package/dist/providers/rolloutWalker.d.ts +53 -0
  86. package/dist/providers/rolloutWalker.js +209 -0
  87. package/dist/providers/types.d.ts +82 -2
  88. package/dist/providers/types.js +4 -0
  89. package/dist/quota.d.ts +34 -4
  90. package/dist/quota.js +37 -9
  91. package/dist/quotaHistory.d.ts +3 -2
  92. package/dist/quotaHistory.js +36 -35
  93. package/dist/quotaPoller.d.ts +13 -1
  94. package/dist/quotaPoller.js +65 -3
  95. package/dist/quotaResolver.d.ts +81 -0
  96. package/dist/quotaResolver.js +213 -0
  97. package/dist/quotaSnapshots.d.ts +15 -2
  98. package/dist/quotaSnapshots.js +25 -3
  99. package/dist/quotaThresholds.d.ts +60 -0
  100. package/dist/quotaThresholds.js +90 -0
  101. package/dist/report/htmlReportGenerator.js +10 -4
  102. package/dist/report/sessionReportInputs.d.ts +23 -0
  103. package/dist/report/sessionReportInputs.js +34 -0
  104. package/dist/schemas/accountManager.d.ts +2 -2
  105. package/dist/schemas/accountManager.js +2 -4
  106. package/dist/schemas/accountStatus.d.ts +3 -0
  107. package/dist/schemas/accountStatus.js +1 -0
  108. package/dist/schemas/index.d.ts +3 -0
  109. package/dist/schemas/index.js +16 -2
  110. package/dist/schemas/observedSessionV1.d.ts +27 -26
  111. package/dist/schemas/observedSessionV1.js +6 -4
  112. package/dist/schemas/quota.d.ts +141 -2
  113. package/dist/schemas/quota.js +20 -8
  114. package/dist/schemas/quotaHistory.d.ts +3 -2
  115. package/dist/schemas/quotaHistory.js +1 -1
  116. package/dist/schemas/sessionEvent.d.ts +6 -0
  117. package/dist/schemas/sessionEvent.js +1 -0
  118. package/dist/schemas/stateFile.d.ts +198 -0
  119. package/dist/schemas/stateFile.js +77 -0
  120. package/dist/schemas/tokenEstimation.d.ts +1 -1
  121. package/dist/schemas/transcript.js +2 -0
  122. package/dist/sessionFingerprint.d.ts +25 -0
  123. package/dist/sessionFingerprint.js +46 -0
  124. package/dist/sessionMonitor.d.ts +15 -0
  125. package/dist/sessionMonitor.js +142 -0
  126. package/dist/sessionPreviews.d.ts +33 -2
  127. package/dist/sessionPreviews.js +333 -6
  128. package/dist/sessionStats.d.ts +51 -0
  129. package/dist/sessionStats.js +166 -0
  130. package/dist/stateFile.d.ts +113 -0
  131. package/dist/stateFile.js +160 -0
  132. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  133. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  134. package/dist/statusline/formatter.d.ts +13 -0
  135. package/dist/statusline/formatter.js +48 -6
  136. package/dist/statusline/index.d.ts +10 -2
  137. package/dist/statusline/index.js +19 -1
  138. package/dist/tokenSummary.d.ts +62 -0
  139. package/dist/tokenSummary.js +72 -0
  140. package/dist/transcript.d.ts +5 -0
  141. package/dist/transcript.js +2 -0
  142. package/dist/types/observedSessionV1.d.ts +8 -2
  143. package/dist/types/observedSessionV1.js +88 -25
  144. package/dist/types/sessionEvent.d.ts +2 -0
  145. package/dist/usage/billingBlocks.d.ts +85 -0
  146. package/dist/usage/billingBlocks.js +153 -0
  147. package/dist/usage/importSessionHistory.d.ts +53 -0
  148. package/dist/usage/importSessionHistory.js +88 -0
  149. package/dist/usage/usageEvents.d.ts +92 -0
  150. package/dist/usage/usageEvents.js +322 -0
  151. package/dist/usage/usageReports.d.ts +76 -0
  152. package/dist/usage/usageReports.js +208 -0
  153. package/dist/watchers/factory.d.ts +6 -0
  154. package/dist/watchers/factory.js +30 -25
  155. package/dist/zaiQuotaApi.d.ts +2 -0
  156. package/dist/zaiQuotaApi.js +2 -0
  157. package/package.json +22 -1
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.enrichCodexQuota = enrichCodexQuota;
36
37
  exports.quotaFromCodexRateLimits = quotaFromCodexRateLimits;
37
38
  exports.readLatestCodexQuotaFromRollouts = readLatestCodexQuotaFromRollouts;
38
39
  exports.resolveCodexQuotaFromLocalSources = resolveCodexQuotaFromLocalSources;
@@ -45,6 +46,7 @@ const codexProfiles_1 = require("./codexProfiles");
45
46
  const quotaSnapshots_1 = require("./quotaSnapshots");
46
47
  const quota_1 = require("./quota");
47
48
  const codex_1 = require("./providers/codex");
49
+ const rolloutWalker_1 = require("./providers/rolloutWalker");
48
50
  const codex_2 = require("./types/codex");
49
51
  const DEFAULT_TAIL_BYTES = 2 * 1024 * 1024;
50
52
  const DEFAULT_MAX_SESSION_FILES = 50;
@@ -105,6 +107,7 @@ function timestampToIso(seconds) {
105
107
  function accountEmail(account) {
106
108
  return account?.email ?? account?.metadata?.email;
107
109
  }
110
+ /** Stamp a Codex quota sample with its runtime provider, projections, and account identity. */
108
111
  function enrichCodexQuota(state, account) {
109
112
  const withProjections = (0, quota_1.withQuotaProjections)(state);
110
113
  return {
@@ -278,39 +281,14 @@ function readLatestCodexQuotaHitFromRollouts(sessionPaths, options = {}) {
278
281
  }
279
282
  return latest;
280
283
  }
281
- function dedupePaths(paths) {
282
- const seen = new Set();
283
- const unique = [];
284
- for (const filePath of paths) {
285
- if (seen.has(filePath))
286
- continue;
287
- seen.add(filePath);
288
- unique.push(filePath);
289
- }
290
- return unique;
291
- }
292
- function sortPathsByMtimeDesc(paths) {
293
- return [...paths].sort((a, b) => {
294
- const aMtime = safeMtimeMs(a);
295
- const bMtime = safeMtimeMs(b);
296
- return bMtime - aMtime;
297
- });
298
- }
299
- function safeMtimeMs(filePath) {
300
- try {
301
- return fs.statSync(filePath).mtime.getTime();
302
- }
303
- catch {
304
- return 0;
305
- }
306
- }
307
- function findAccountRolloutFiles(codexHome) {
284
+ /**
285
+ * The newest account-level rollouts, one capped walk across the monitored
286
+ * homes. `limit` bounds the walk itself, so a large history is never fully
287
+ * enumerated or stat'ed just to tail a handful of files.
288
+ */
289
+ function findAccountRolloutFiles(codexHome, limit) {
308
290
  const homes = codexHome ? [codexHome] : (0, codexProfiles_1.getCodexMonitoringHomes)();
309
- const files = [];
310
- for (const home of homes) {
311
- files.push(...findRolloutFiles(path.join(home, 'sessions')));
312
- }
313
- return sortPathsByMtimeDesc(dedupePaths(files));
291
+ return (0, rolloutWalker_1.walkRolloutFiles)(homes.map((home) => path.join(home, 'sessions')), { limit }).map((file) => file.path);
314
292
  }
315
293
  function resolveCodexQuotaFromLocalSources(options = {}) {
316
294
  const account = options.activeAccount !== undefined ? options.activeAccount : (0, codexProfiles_1.getActiveCodexAccount)();
@@ -335,7 +313,7 @@ function resolveCodexQuotaFromLocalSources(options = {}) {
335
313
  if (workspaceHit)
336
314
  candidates.push(workspaceHit);
337
315
  }
338
- const accountSessions = findAccountRolloutFiles(options.codexHome);
316
+ const accountSessions = findAccountRolloutFiles(options.codexHome, maxSessionFiles);
339
317
  const accountHit = readLatestCodexQuotaHitFromRollouts(accountSessions, {
340
318
  maxTailBytes,
341
319
  maxSessionFiles,
@@ -626,36 +604,3 @@ function readLatestQuotaFromRollout(sessionPath, maxTailBytes, source) {
626
604
  }
627
605
  return null;
628
606
  }
629
- function findRolloutFiles(sessionsDir) {
630
- const results = [];
631
- function visit(dir) {
632
- let entries;
633
- try {
634
- entries = fs.readdirSync(dir, { withFileTypes: true });
635
- }
636
- catch {
637
- return;
638
- }
639
- for (const entry of entries) {
640
- const fullPath = path.join(dir, entry.name);
641
- if (entry.isDirectory()) {
642
- visit(fullPath);
643
- continue;
644
- }
645
- if (!entry.isFile() || !entry.name.startsWith('rollout-') || !entry.name.endsWith('.jsonl'))
646
- continue;
647
- try {
648
- const stat = fs.statSync(fullPath);
649
- if (stat.size > 0) {
650
- results.push({ path: fullPath, mtime: stat.mtime.getTime() });
651
- }
652
- }
653
- catch {
654
- // Skip inaccessible files.
655
- }
656
- }
657
- }
658
- visit(sessionsDir);
659
- results.sort((a, b) => b.mtime - a.mtime);
660
- return results.map((item) => item.path);
661
- }
@@ -6,6 +6,7 @@ import type { SavedAccountProfile } from './accountRegistry';
6
6
  import type { Disposable } from './quotaPoller';
7
7
  import type { QuotaState } from './quota';
8
8
  import type { ProviderQuotaState } from './providerQuota';
9
+ import { type AccountsChangedEvent } from './accountChanges';
9
10
  type CodexAccountReader = () => SavedAccountProfile | null;
10
11
  type SnapshotReader = (providerId: 'codex', accountId: string) => QuotaState | null;
11
12
  type SnapshotWriter = (providerId: 'codex', accountId: string, quota: QuotaState) => void;
@@ -27,6 +28,7 @@ export interface CodexQuotaWatcherOptions {
27
28
  workspaceId?: string;
28
29
  /** Override the history append function (used by tests). Default: `appendQuotaHistorySample`. */
29
30
  appendHistorySample?: HistoryAppender;
31
+ subscribeAccountsChanged?: (listener: (event: AccountsChangedEvent) => void) => Disposable;
30
32
  }
31
33
  /**
32
34
  * Watches the active Codex rollout for quota snapshots and falls back to the
@@ -46,6 +48,7 @@ export declare class CodexQuotaWatcher implements Disposable {
46
48
  private readonly now;
47
49
  private readonly workspaceId;
48
50
  private readonly appendHistorySample;
51
+ private readonly subscribeAccountsChanged;
49
52
  private readonly listeners;
50
53
  private discoveryTimer;
51
54
  private provider;
@@ -56,6 +59,7 @@ export declare class CodexQuotaWatcher implements Disposable {
56
59
  private lastLocalScanAt;
57
60
  private lastLocalScanState;
58
61
  private running;
62
+ private accountSubscription;
59
63
  constructor(workspacePath: string, options?: CodexQuotaWatcherOptions);
60
64
  start(): void;
61
65
  stop(): void;
@@ -63,12 +67,14 @@ export declare class CodexQuotaWatcher implements Disposable {
63
67
  onUpdate(cb: (state: ProviderQuotaState<'codex'>) => void): Disposable;
64
68
  onQuotaUpdate(cb: (state: ProviderQuotaState<'codex'>) => void): Disposable;
65
69
  refresh(): void;
70
+ private handleAccountsChanged;
66
71
  private refreshActiveSession;
67
72
  private attachToSession;
68
73
  private ingestSessionUpdate;
69
74
  private emitCachedOrUnavailable;
70
75
  private emitState;
71
76
  private getProvider;
77
+ private safeGetActiveAccount;
72
78
  private teardownActiveSession;
73
79
  }
74
80
  export {};
@@ -40,6 +40,7 @@ const codexQuota_1 = require("./codexQuota");
40
40
  const quotaSnapshots_1 = require("./quotaSnapshots");
41
41
  const quotaHistory_1 = require("./quotaHistory");
42
42
  const codex_1 = require("./providers/codex");
43
+ const accountChanges_1 = require("./accountChanges");
43
44
  const DEFAULT_DISCOVERY_POLL_INTERVAL_MS = 30_000;
44
45
  const DEFAULT_LOCAL_SCAN_CACHE_MS = 5 * 60_000;
45
46
  function accountEmail(account) {
@@ -86,6 +87,7 @@ class CodexQuotaWatcher {
86
87
  now;
87
88
  workspaceId;
88
89
  appendHistorySample;
90
+ subscribeAccountsChanged;
89
91
  listeners = [];
90
92
  discoveryTimer;
91
93
  provider = null;
@@ -96,6 +98,7 @@ class CodexQuotaWatcher {
96
98
  lastLocalScanAt = Number.NEGATIVE_INFINITY;
97
99
  lastLocalScanState = null;
98
100
  running = false;
101
+ accountSubscription = null;
99
102
  constructor(workspacePath, options = {}) {
100
103
  this.workspacePath = workspacePath;
101
104
  this.discoveryPollIntervalMs =
@@ -119,15 +122,14 @@ class CodexQuotaWatcher {
119
122
  this.now = options.now ?? Date.now;
120
123
  this.workspaceId = options.workspaceId;
121
124
  this.appendHistorySample = options.appendHistorySample ?? quotaHistory_1.appendQuotaHistorySample;
125
+ this.subscribeAccountsChanged = options.subscribeAccountsChanged ?? accountChanges_1.onAccountsChanged;
122
126
  }
123
127
  start() {
124
128
  if (this.running)
125
129
  return;
126
130
  this.running = true;
127
- this.refreshActiveSession();
128
- this.discoveryTimer = setInterval(() => {
129
- this.refreshActiveSession();
130
- }, this.discoveryPollIntervalMs);
131
+ this.accountSubscription ??= this.subscribeAccountsChanged(() => this.handleAccountsChanged());
132
+ this.handleAccountsChanged();
131
133
  }
132
134
  stop() {
133
135
  if (!this.running)
@@ -137,6 +139,8 @@ class CodexQuotaWatcher {
137
139
  clearInterval(this.discoveryTimer);
138
140
  this.discoveryTimer = undefined;
139
141
  }
142
+ this.accountSubscription?.dispose();
143
+ this.accountSubscription = null;
140
144
  this.teardownActiveSession();
141
145
  }
142
146
  dispose() {
@@ -157,21 +161,46 @@ class CodexQuotaWatcher {
157
161
  return this.onUpdate(cb);
158
162
  }
159
163
  refresh() {
160
- this.refreshActiveSession();
164
+ this.handleAccountsChanged();
161
165
  }
162
- refreshActiveSession() {
166
+ handleAccountsChanged() {
167
+ if (!this.running)
168
+ return;
169
+ const account = this.safeGetActiveAccount();
170
+ if (!account) {
171
+ if (this.discoveryTimer) {
172
+ clearInterval(this.discoveryTimer);
173
+ this.discoveryTimer = undefined;
174
+ }
175
+ this.teardownActiveSession();
176
+ this.emitState(makeUnavailableState(null, 'No Codex account configured'));
177
+ return;
178
+ }
179
+ if (!this.discoveryTimer) {
180
+ this.discoveryTimer = setInterval(() => {
181
+ this.refreshActiveSession();
182
+ }, this.discoveryPollIntervalMs);
183
+ this.discoveryTimer.unref?.();
184
+ }
185
+ this.refreshActiveSession(account);
186
+ }
187
+ refreshActiveSession(account = this.safeGetActiveAccount()) {
188
+ if (!account) {
189
+ this.handleAccountsChanged();
190
+ return;
191
+ }
163
192
  const provider = this.getProvider();
164
193
  const nextSessionPath = provider.findActiveSession(this.workspacePath);
165
194
  if (!nextSessionPath) {
166
195
  this.teardownActiveSession();
167
- this.emitCachedOrUnavailable();
196
+ this.emitCachedOrUnavailable(account);
168
197
  return;
169
198
  }
170
199
  if (nextSessionPath !== this.sessionPath || this.reader == null) {
171
200
  this.attachToSession(nextSessionPath);
172
201
  return;
173
202
  }
174
- this.ingestSessionUpdate('readNew');
203
+ this.ingestSessionUpdate('readNew', account);
175
204
  }
176
205
  attachToSession(nextSessionPath) {
177
206
  this.teardownActiveSession();
@@ -189,14 +218,30 @@ class CodexQuotaWatcher {
189
218
  }
190
219
  this.refreshActiveSession();
191
220
  });
221
+ if (typeof this.fileWatcher.on === 'function') {
222
+ this.fileWatcher.on('error', () => {
223
+ try {
224
+ this.fileWatcher?.close();
225
+ }
226
+ catch {
227
+ // Discovery polling remains active.
228
+ }
229
+ this.fileWatcher = null;
230
+ });
231
+ }
192
232
  }
193
233
  catch {
194
234
  this.emitCachedOrUnavailable();
195
235
  }
196
236
  }
197
- ingestSessionUpdate(mode) {
237
+ ingestSessionUpdate(mode, knownAccount) {
238
+ const account = knownAccount ?? this.safeGetActiveAccount();
239
+ if (!account) {
240
+ this.handleAccountsChanged();
241
+ return;
242
+ }
198
243
  if (!this.provider || !this.reader) {
199
- this.emitCachedOrUnavailable();
244
+ this.emitCachedOrUnavailable(account);
200
245
  return;
201
246
  }
202
247
  if (!this.reader.exists()) {
@@ -211,48 +256,45 @@ class CodexQuotaWatcher {
211
256
  }
212
257
  const liveQuota = (0, codexQuota_1.quotaFromCodexRateLimits)(this.provider.getLastRateLimits(), 'session');
213
258
  if (!liveQuota) {
214
- this.emitCachedOrUnavailable();
259
+ this.emitCachedOrUnavailable(account);
215
260
  return;
216
261
  }
217
- const account = this.getActiveAccount();
218
- const cached = account ? this.readSnapshot('codex', account.id) : null;
262
+ const cached = this.readSnapshot('codex', account.id);
219
263
  const liveQuotaWithResetCredits = {
220
264
  ...liveQuota,
221
265
  resetCredits: liveQuota.resetCredits ?? cached?.resetCredits,
222
266
  };
223
- if (account) {
224
- this.writeSnapshot('codex', account.id, liveQuotaWithResetCredits);
225
- if (this.workspaceId) {
226
- const sample = {
227
- timestamp: liveQuotaWithResetCredits.capturedAt ?? new Date().toISOString(),
228
- runtimeProvider: 'codex',
229
- providerId: account.id,
230
- workspaceId: this.workspaceId,
231
- fiveHour: {
232
- utilization: liveQuotaWithResetCredits.fiveHour.utilization,
233
- resetsAt: liveQuotaWithResetCredits.fiveHour.resetsAt,
234
- },
235
- sevenDay: {
236
- utilization: liveQuotaWithResetCredits.sevenDay.utilization,
237
- resetsAt: liveQuotaWithResetCredits.sevenDay.resetsAt,
238
- },
239
- available: liveQuotaWithResetCredits.available,
240
- error: liveQuotaWithResetCredits.error,
241
- source: liveQuotaWithResetCredits.source,
242
- stale: liveQuotaWithResetCredits.stale,
243
- };
244
- try {
245
- const result = this.appendHistorySample(sample);
246
- if (result && typeof result.catch === 'function') {
247
- result.catch(() => {
248
- // History append must never break the live emission path.
249
- });
250
- }
251
- }
252
- catch {
253
- // Synchronous errors swallowed for the same reason.
267
+ this.writeSnapshot('codex', account.id, liveQuotaWithResetCredits);
268
+ if (this.workspaceId) {
269
+ const sample = {
270
+ timestamp: liveQuotaWithResetCredits.capturedAt ?? new Date().toISOString(),
271
+ runtimeProvider: 'codex',
272
+ providerId: account.id,
273
+ workspaceId: this.workspaceId,
274
+ fiveHour: {
275
+ utilization: liveQuotaWithResetCredits.fiveHour.utilization,
276
+ resetsAt: liveQuotaWithResetCredits.fiveHour.resetsAt,
277
+ },
278
+ sevenDay: {
279
+ utilization: liveQuotaWithResetCredits.sevenDay.utilization,
280
+ resetsAt: liveQuotaWithResetCredits.sevenDay.resetsAt,
281
+ },
282
+ available: liveQuotaWithResetCredits.available,
283
+ error: liveQuotaWithResetCredits.error,
284
+ source: liveQuotaWithResetCredits.source,
285
+ stale: liveQuotaWithResetCredits.stale,
286
+ };
287
+ try {
288
+ const result = this.appendHistorySample(sample);
289
+ if (result && typeof result.catch === 'function') {
290
+ result.catch(() => {
291
+ // History append must never break the live emission path.
292
+ });
254
293
  }
255
294
  }
295
+ catch {
296
+ // Synchronous errors swallowed for the same reason.
297
+ }
256
298
  }
257
299
  this.emitState(enrichQuotaState({
258
300
  ...liveQuotaWithResetCredits,
@@ -260,8 +302,12 @@ class CodexQuotaWatcher {
260
302
  providerId: 'codex',
261
303
  }, account));
262
304
  }
263
- emitCachedOrUnavailable() {
264
- const account = this.getActiveAccount();
305
+ emitCachedOrUnavailable(knownAccount) {
306
+ const account = knownAccount ?? this.safeGetActiveAccount();
307
+ if (!account) {
308
+ this.emitState(makeUnavailableState(null, 'No Codex account configured'));
309
+ return;
310
+ }
265
311
  const scanNow = this.now();
266
312
  if (scanNow - this.lastLocalScanAt < this.localScanCacheMs) {
267
313
  if (this.lastLocalScanState) {
@@ -296,7 +342,7 @@ class CodexQuotaWatcher {
296
342
  localProvider?.dispose();
297
343
  }
298
344
  }
299
- const cached = account ? this.readSnapshot('codex', account.id) : null;
345
+ const cached = this.readSnapshot('codex', account.id);
300
346
  if (cached) {
301
347
  this.emitState(enrichQuotaState({
302
348
  ...cached,
@@ -331,6 +377,14 @@ class CodexQuotaWatcher {
331
377
  }
332
378
  return this.provider;
333
379
  }
380
+ safeGetActiveAccount() {
381
+ try {
382
+ return this.getActiveAccount();
383
+ }
384
+ catch {
385
+ return null;
386
+ }
387
+ }
334
388
  teardownActiveSession() {
335
389
  if (this.fileWatcher) {
336
390
  this.fileWatcher.close();
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Failing-tool trend rows shared by the CLI `stats` block and the VS Code
3
+ * Health tab. Browser-safe: no filesystem access; callers pass the two
4
+ * windows they read with `getTopFailingTools()`.
5
+ *
6
+ * @module failingTools
7
+ */
8
+ /** The shape `getTopFailingTools()` returns; declared structurally to stay fs-free. */
9
+ export interface FailingToolWindow {
10
+ tool: string;
11
+ failures: number;
12
+ categories?: Record<string, number>;
13
+ }
14
+ export type FailingToolTrend = 'up' | 'down' | 'flat';
15
+ export interface FailingToolTrendRow {
16
+ tool: string;
17
+ /** Failures in the last 7 days. */
18
+ last7: number;
19
+ /** Failures in the last 30 days. */
20
+ last30: number;
21
+ /** Category counts over the 30-day window (or the 7-day one when only that is known). */
22
+ categories: Record<string, number>;
23
+ /** Whether the last week is worse, better, or in line with the 30-day weekly average. */
24
+ trend: FailingToolTrend;
25
+ }
26
+ /**
27
+ * Compare the last week against the 30-day weekly average: more than 10%
28
+ * above is `up`, more than 10% below is `down`, otherwise `flat`. A tool
29
+ * with no 30-day history but failures this week is `up`.
30
+ */
31
+ export declare function failingToolTrend(last7: number, last30: number): FailingToolTrend;
32
+ export declare const FAILING_TOOL_TREND_ARROWS: Record<FailingToolTrend, string>;
33
+ /**
34
+ * Merge the 7-day and 30-day windows into one row per tool, ordered by
35
+ * 7-day failures, then 30-day failures, then name.
36
+ */
37
+ export declare function mergeFailingToolWindows(last7: FailingToolWindow[], last30: FailingToolWindow[]): FailingToolTrendRow[];
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Failing-tool trend rows shared by the CLI `stats` block and the VS Code
4
+ * Health tab. Browser-safe: no filesystem access; callers pass the two
5
+ * windows they read with `getTopFailingTools()`.
6
+ *
7
+ * @module failingTools
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.FAILING_TOOL_TREND_ARROWS = void 0;
11
+ exports.failingToolTrend = failingToolTrend;
12
+ exports.mergeFailingToolWindows = mergeFailingToolWindows;
13
+ const TREND_TOLERANCE = 0.1;
14
+ /**
15
+ * Compare the last week against the 30-day weekly average: more than 10%
16
+ * above is `up`, more than 10% below is `down`, otherwise `flat`. A tool
17
+ * with no 30-day history but failures this week is `up`.
18
+ */
19
+ function failingToolTrend(last7, last30) {
20
+ const weeklyAverage = last30 / (30 / 7);
21
+ if (weeklyAverage <= 0)
22
+ return last7 > 0 ? 'up' : 'flat';
23
+ const ratio = last7 / weeklyAverage;
24
+ if (ratio > 1 + TREND_TOLERANCE)
25
+ return 'up';
26
+ if (ratio < 1 - TREND_TOLERANCE)
27
+ return 'down';
28
+ return 'flat';
29
+ }
30
+ exports.FAILING_TOOL_TREND_ARROWS = {
31
+ up: '↑',
32
+ down: '↓',
33
+ flat: '→',
34
+ };
35
+ /**
36
+ * Merge the 7-day and 30-day windows into one row per tool, ordered by
37
+ * 7-day failures, then 30-day failures, then name.
38
+ */
39
+ function mergeFailingToolWindows(last7, last30) {
40
+ const rows = new Map();
41
+ for (const tool of last30) {
42
+ rows.set(tool.tool, {
43
+ tool: tool.tool,
44
+ last7: 0,
45
+ last30: tool.failures,
46
+ categories: { ...(tool.categories ?? {}) },
47
+ trend: 'flat',
48
+ });
49
+ }
50
+ for (const tool of last7) {
51
+ const row = rows.get(tool.tool);
52
+ if (row) {
53
+ row.last7 = tool.failures;
54
+ // A 7-day count cannot exceed the 30-day one; tolerate windows read at different instants.
55
+ row.last30 = Math.max(row.last30, tool.failures);
56
+ }
57
+ else {
58
+ rows.set(tool.tool, {
59
+ tool: tool.tool,
60
+ last7: tool.failures,
61
+ last30: tool.failures,
62
+ categories: { ...(tool.categories ?? {}) },
63
+ trend: 'flat',
64
+ });
65
+ }
66
+ }
67
+ for (const row of rows.values())
68
+ row.trend = failingToolTrend(row.last7, row.last30);
69
+ return [...rows.values()].sort((a, b) => b.last7 - a.last7 || b.last30 - a.last30 || a.tool.localeCompare(b.tool));
70
+ }
@@ -15,7 +15,9 @@ exports.fmtTokens = fmtTokens;
15
15
  exports.fmtCost = fmtCost;
16
16
  exports.formatTimestamp = formatTimestamp;
17
17
  exports.formatDuration = formatDuration;
18
+ const costProvenance_1 = require("../aggregation/costProvenance");
18
19
  const formatting_1 = require("../formatting");
20
+ const tokenSummary_1 = require("../tokenSummary");
19
21
  // ── JSON output ──
20
22
  /**
21
23
  * Serialize aggregated metrics as pretty-printed JSON.
@@ -128,9 +130,12 @@ function formatSessionMarkdown(metrics, options = {}) {
128
130
  lines.push(`| Output | ${fmtTokens(metrics.tokens.outputTokens)} |`);
129
131
  lines.push(`| Cache write | ${fmtTokens(metrics.tokens.cacheWriteTokens)} |`);
130
132
  lines.push(`| Cache read | ${fmtTokens(metrics.tokens.cacheReadTokens)} |`);
131
- lines.push(`| Total | ${fmtTokens(metrics.tokens.inputTokens + metrics.tokens.outputTokens)} |`);
132
- if (metrics.tokens.reportedCost > 0) {
133
- lines.push(`| Cost | ${fmtCost(metrics.tokens.reportedCost)} |`);
133
+ lines.push(`| ${tokenSummary_1.TOKEN_TOTAL_LABEL} | ${fmtTokens((0, tokenSummary_1.summarizeTokens)(metrics.tokens).total)} |`);
134
+ if (metrics.tokens.costUsd > 0) {
135
+ lines.push(`| Cost | ${fmtCost(metrics.tokens.costUsd)} (${(0, costProvenance_1.describeCostProvenance)(metrics.tokens)}) |`);
136
+ }
137
+ else if (metrics.tokens.unpricedCalls > 0) {
138
+ lines.push(`| Cost | — (${(0, costProvenance_1.describeCostProvenance)(metrics.tokens)}) |`);
134
139
  }
135
140
  lines.push('');
136
141
  // Model stats
@@ -222,9 +227,9 @@ function formatHeader(metrics) {
222
227
  }
223
228
  function formatTokenSummary(metrics) {
224
229
  const t = metrics.tokens;
225
- const total = t.inputTokens + t.outputTokens;
230
+ const total = (0, tokenSummary_1.summarizeTokens)(t).total;
226
231
  const parts = [
227
- `Tokens: ${fmtTokens(total)} total`,
232
+ `Tokens: ${fmtTokens(total)} total incl. cache`,
228
233
  `(${fmtTokens(t.inputTokens)} in`,
229
234
  `${fmtTokens(t.outputTokens)} out`,
230
235
  ];
@@ -233,8 +238,9 @@ function formatTokenSummary(metrics) {
233
238
  if (t.cacheWriteTokens > 0)
234
239
  parts.push(`${fmtTokens(t.cacheWriteTokens)} cache-write`);
235
240
  const line = parts.join(', ');
236
- if (t.reportedCost > 0)
237
- return `${line}) Cost: ${fmtCost(t.reportedCost)}`;
241
+ if (t.costUsd > 0) {
242
+ return `${line}) Cost: ${fmtCost(t.costUsd)} (${(0, costProvenance_1.describeCostProvenance)(t)})`;
243
+ }
238
244
  return line + ')';
239
245
  }
240
246
  /** @internal Exported for tests. */
@@ -24,3 +24,16 @@ export interface FormatDurationMsOptions {
24
24
  export declare function formatTokenCount(value: number, options?: FormatTokenCountOptions): string;
25
25
  /** Format an elapsed duration in milliseconds. */
26
26
  export declare function formatDurationMs(ms: number, options?: FormatDurationMsOptions): string;
27
+ /**
28
+ * Calendar-date key (`YYYY-MM-DD`) in the machine's local time zone.
29
+ *
30
+ * Sidekick buckets "today", daily history, and heatmaps by the user's local
31
+ * day, so every writer and reader of a date key goes through this helper.
32
+ * A UTC key would move late-evening work onto the next day for anyone west
33
+ * of Greenwich and onto the previous day for anyone east of it.
34
+ */
35
+ export declare function formatLocalDateKey(value?: Date | number): string;
36
+ /** Parse a `YYYY-MM-DD` key produced by `formatLocalDateKey` back into local midnight. */
37
+ export declare function parseLocalDateKey(key: string): Date | null;
38
+ /** Shift a `YYYY-MM-DD` key by whole local days (DST-safe). */
39
+ export declare function addLocalDays(key: string, days: number): string;
@@ -10,6 +10,9 @@
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.formatTokenCount = formatTokenCount;
12
12
  exports.formatDurationMs = formatDurationMs;
13
+ exports.formatLocalDateKey = formatLocalDateKey;
14
+ exports.parseLocalDateKey = parseLocalDateKey;
15
+ exports.addLocalDays = addLocalDays;
13
16
  /** Format a token count with compact `k` / `M` suffixes. */
14
17
  function formatTokenCount(value, options = {}) {
15
18
  if (!Number.isFinite(value))
@@ -52,3 +55,35 @@ function formatDurationMs(ms, options = {}) {
52
55
  }
53
56
  return `${minutes}m${separator}${remainingSeconds}s`;
54
57
  }
58
+ /**
59
+ * Calendar-date key (`YYYY-MM-DD`) in the machine's local time zone.
60
+ *
61
+ * Sidekick buckets "today", daily history, and heatmaps by the user's local
62
+ * day, so every writer and reader of a date key goes through this helper.
63
+ * A UTC key would move late-evening work onto the next day for anyone west
64
+ * of Greenwich and onto the previous day for anyone east of it.
65
+ */
66
+ function formatLocalDateKey(value = new Date()) {
67
+ const date = typeof value === 'number' ? new Date(value) : value;
68
+ if (Number.isNaN(date.getTime()))
69
+ return '';
70
+ const year = date.getFullYear();
71
+ const month = String(date.getMonth() + 1).padStart(2, '0');
72
+ const day = String(date.getDate()).padStart(2, '0');
73
+ return `${year}-${month}-${day}`;
74
+ }
75
+ /** Parse a `YYYY-MM-DD` key produced by `formatLocalDateKey` back into local midnight. */
76
+ function parseLocalDateKey(key) {
77
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
78
+ if (!match)
79
+ return null;
80
+ const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
81
+ return Number.isNaN(date.getTime()) ? null : date;
82
+ }
83
+ /** Shift a `YYYY-MM-DD` key by whole local days (DST-safe). */
84
+ function addLocalDays(key, days) {
85
+ const date = parseLocalDateKey(key);
86
+ if (!date)
87
+ return key;
88
+ return formatLocalDateKey(new Date(date.getFullYear(), date.getMonth(), date.getDate() + days));
89
+ }