sidekick-shared 0.23.0 → 0.24.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 (90) hide show
  1. package/README.md +139 -57
  2. package/dist/accountManager.js +10 -0
  3. package/dist/aggregation/EventAggregator.d.ts +8 -1
  4. package/dist/aggregation/EventAggregator.js +82 -136
  5. package/dist/aggregation/snapshot.d.ts +1 -1
  6. package/dist/aggregation/snapshot.js +2 -1
  7. package/dist/aggregation/types.d.ts +5 -0
  8. package/dist/autoSwitch.d.ts +1 -1
  9. package/dist/autoSwitch.js +14 -3
  10. package/dist/browser.d.ts +6 -0
  11. package/dist/browser.js +10 -1
  12. package/dist/codexQuota.d.ts +2 -0
  13. package/dist/codexQuota.js +27 -0
  14. package/dist/codexQuotaWatcher.d.ts +7 -0
  15. package/dist/codexQuotaWatcher.js +37 -19
  16. package/dist/context/sessionContext.js +43 -15
  17. package/dist/credentialIO.js +19 -5
  18. package/dist/errorHistory.d.ts +1 -0
  19. package/dist/errorHistory.js +3 -2
  20. package/dist/extractors/tokenUsage.js +1 -1
  21. package/dist/extractors/toolCall.js +2 -2
  22. package/dist/index.d.ts +14 -4
  23. package/dist/index.js +25 -7
  24. package/dist/modelInfo.d.ts +17 -7
  25. package/dist/modelInfo.js +11 -2
  26. package/dist/multiProviderQuotaService.js +13 -2
  27. package/dist/node.d.ts +4 -0
  28. package/dist/node.js +8 -1
  29. package/dist/observedSessionCollector.d.ts +68 -0
  30. package/dist/observedSessionCollector.js +178 -0
  31. package/dist/parsers/codexParser.js +24 -6
  32. package/dist/parsers/openCodeParser.js +22 -1
  33. package/dist/parsers/sessionActivityDetector.js +2 -0
  34. package/dist/parsers/sessionPathResolver.js +19 -16
  35. package/dist/parsers/subagentScanner.d.ts +1 -1
  36. package/dist/parsers/subagentScanner.js +4 -5
  37. package/dist/parsers/subagentTraceParser.js +15 -12
  38. package/dist/providerStatus.js +13 -2
  39. package/dist/providers/claudeCode.js +63 -5
  40. package/dist/providers/codex.js +56 -17
  41. package/dist/providers/detect.js +32 -3
  42. package/dist/providers/openCodeDatabase.js +12 -3
  43. package/dist/quota.js +8 -0
  44. package/dist/quotaPoller.js +17 -6
  45. package/dist/quotaSnapshots.js +80 -26
  46. package/dist/readers/decisions.js +3 -1
  47. package/dist/readers/handoff.js +1 -1
  48. package/dist/readers/notes.js +9 -4
  49. package/dist/readers/plans.js +2 -4
  50. package/dist/readers/tasks.js +3 -1
  51. package/dist/report/htmlHelpers.js +6 -1
  52. package/dist/report/openBrowser.js +3 -3
  53. package/dist/report/transcriptParser.d.ts +3 -11
  54. package/dist/report/transcriptParser.js +69 -154
  55. package/dist/schemas/index.d.ts +7 -1
  56. package/dist/schemas/index.js +18 -2
  57. package/dist/schemas/observedSessionV1.d.ts +32 -1
  58. package/dist/schemas/observedSessionV1.js +11 -1
  59. package/dist/schemas/sessionEvent.d.ts +353 -12
  60. package/dist/schemas/sessionEvent.js +76 -14
  61. package/dist/schemas/tokenEstimation.d.ts +14 -0
  62. package/dist/schemas/tokenEstimation.js +10 -0
  63. package/dist/schemas/transcript.d.ts +24 -0
  64. package/dist/schemas/transcript.js +81 -0
  65. package/dist/schemas/usageNormalization.d.ts +55 -0
  66. package/dist/schemas/usageNormalization.js +41 -0
  67. package/dist/sessionTranscripts.d.ts +16 -0
  68. package/dist/sessionTranscripts.js +47 -0
  69. package/dist/statusline/formatter.js +8 -1
  70. package/dist/terminalSync.js +16 -7
  71. package/dist/tokenEstimation.d.ts +24 -0
  72. package/dist/tokenEstimation.js +62 -0
  73. package/dist/transcript.d.ts +92 -0
  74. package/dist/transcript.js +240 -0
  75. package/dist/types/historicalData.d.ts +14 -0
  76. package/dist/types/historicalData.js +22 -0
  77. package/dist/types/observedSessionV1.d.ts +3 -1
  78. package/dist/types/observedSessionV1.js +20 -6
  79. package/dist/types/sessionEvent.d.ts +66 -7
  80. package/dist/usageNormalization.d.ts +69 -0
  81. package/dist/usageNormalization.js +100 -0
  82. package/dist/watchers/factory.js +14 -3
  83. package/dist/watchers/jsonlTail.js +34 -14
  84. package/dist/watchers/jsonlWatcher.js +5 -3
  85. package/dist/watchers/providerReaderWatcher.js +4 -2
  86. package/dist/watchers/sqliteWatcher.d.ts +2 -0
  87. package/dist/watchers/sqliteWatcher.js +27 -14
  88. package/dist/writers/atomic.js +100 -13
  89. package/dist/zaiQuotaWatcher.js +6 -0
  90. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  * @module aggregation/EventAggregator
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.EventAggregator = void 0;
12
+ exports.EventAggregator = exports.SNAPSHOT_SCHEMA_VERSION = void 0;
13
13
  exports.parseTodoDependencies = parseTodoDependencies;
14
14
  const jsonl_1 = require("../parsers/jsonl");
15
15
  const planExtractor_1 = require("../parsers/planExtractor");
@@ -19,16 +19,18 @@ const noiseClassifier_1 = require("../formatters/noiseClassifier");
19
19
  const FrequencyTracker_1 = require("./FrequencyTracker");
20
20
  const HeatmapTracker_1 = require("./HeatmapTracker");
21
21
  const PatternExtractor_1 = require("./PatternExtractor");
22
- const modelInfo_1 = require("../modelInfo");
23
22
  const errorTaxonomy_1 = require("../extractors/errorTaxonomy");
23
+ const tokenEstimation_1 = require("../tokenEstimation");
24
+ const usageNormalization_1 = require("../usageNormalization");
24
25
  // ── Defaults ──
25
26
  const DEFAULT_TIMELINE_CAP = 200;
26
27
  const DEFAULT_LATENCY_CAP = 100;
28
+ const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
27
29
  const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
28
30
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
29
31
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
30
32
  /** Schema version for serialized snapshots. */
31
- const SNAPSHOT_SCHEMA_VERSION = 3;
33
+ exports.SNAPSHOT_SCHEMA_VERSION = 4;
32
34
  // ── Goal gate detection regex ──
33
35
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
34
36
  // ── Todo dependency parsing ──
@@ -74,6 +76,8 @@ class EventAggregator {
74
76
  outputTokens = 0;
75
77
  cacheWriteTokens = 0;
76
78
  cacheReadTokens = 0;
79
+ reasoningTokens = 0;
80
+ totalTokens = 0;
77
81
  reportedCost = 0;
78
82
  // Per-model usage
79
83
  modelUsage = new Map();
@@ -127,6 +131,7 @@ class EventAggregator {
127
131
  permissionModeHistory = [];
128
132
  // Context timeline
129
133
  contextTimeline = [];
134
+ contextTimelineCap;
130
135
  contextTurnIndex = 0;
131
136
  // Timeline
132
137
  timeline = [];
@@ -150,6 +155,7 @@ class EventAggregator {
150
155
  this.latencyCap = options?.latencyCap ?? DEFAULT_LATENCY_CAP;
151
156
  this.burnWindowMs = options?.burnWindowMs ?? DEFAULT_BURN_WINDOW_MS;
152
157
  this.burnSampleMs = options?.burnSampleMs ?? DEFAULT_BURN_SAMPLE_MS;
158
+ this.contextTimelineCap = options?.contextTimelineCap ?? DEFAULT_CONTEXT_TIMELINE_CAP;
153
159
  this.computeContextSize = options?.computeContextSize ?? null;
154
160
  this.providerId = options?.providerId ?? null;
155
161
  this._providerId = this.providerId;
@@ -165,18 +171,25 @@ class EventAggregator {
165
171
  this.sessionStartTime = event.timestamp;
166
172
  }
167
173
  this.lastEventTime = event.timestamp;
168
- // Guard: some event types (e.g. 'summary') have no message field in the raw JSONL
174
+ this.recordProviderMetadata(event);
175
+ // Bookkeeping rows can be useful without a message payload.
169
176
  if (!event.message) {
177
+ if (event.type === 'summary') {
178
+ this.recordExplicitCompaction(event.timestamp, event.compaction);
179
+ }
180
+ this.trackPermissionMode(event);
181
+ this.addTimelineFromSessionEvent(event);
182
+ this.feedAnalytics(event.timestamp, event.type === 'summary' ? (event.summary ?? '') : '', this.extractToolNameFromEvent(event));
170
183
  return;
171
184
  }
172
185
  // 2. Track model
173
186
  if (event.message.model) {
174
187
  this.currentModel = event.message.model;
175
188
  }
176
- this.recordProviderMetadata(event);
177
189
  // 3. Skip system/audit events and synthetic token-count events for messageCount
178
190
  const msgId = event.message.id ?? '';
179
- if (event.type !== 'system' && !msgId.startsWith('token-count-')) {
191
+ if ((event.type === 'user' || event.type === 'assistant') &&
192
+ !msgId.startsWith('token-count-')) {
180
193
  this.messageCount++;
181
194
  }
182
195
  // 4. Latency tracking
@@ -184,9 +197,10 @@ class EventAggregator {
184
197
  this.processLatency(event);
185
198
  }
186
199
  // 5. Token accumulation
187
- if (event.message.usage) {
188
- const usageCost = this.accumulateUsage(event.message.usage, event.timestamp, event.message.model);
189
- this.attributePlanUsage(event.message.usage.input_tokens + event.message.usage.output_tokens, usageCost);
200
+ const normalizedUsage = (0, usageNormalization_1.extractNormalizedUsage)(event);
201
+ if (normalizedUsage) {
202
+ const usageCost = this.accumulateUsage(normalizedUsage, event.timestamp, event.message.model);
203
+ this.attributePlanUsage(normalizedUsage.totalTokens, usageCost);
190
204
  }
191
205
  if (event.type !== 'system') {
192
206
  // 6. Tool extraction from content blocks
@@ -232,91 +246,25 @@ class EventAggregator {
232
246
  }
233
247
  // 3. Token accumulation from FollowEvent fields
234
248
  if (event.tokens) {
235
- const inputTok = event.tokens.input;
236
- const outputTok = event.tokens.output;
237
- const cacheRead = event.cacheTokens?.read ?? 0;
238
- const cacheWrite = event.cacheTokens?.write ?? 0;
239
- this.inputTokens += inputTok;
240
- this.outputTokens += outputTok;
241
- this.cacheReadTokens += cacheRead;
242
- this.cacheWriteTokens += cacheWrite;
243
- this.attributePlanUsage(inputTok + outputTok, event.cost ?? 0);
244
- // Tokens since last burn sample
245
- this.tokensSinceLastSample += inputTok + outputTok;
246
- // Context size
247
- let contextSize;
248
- if (this.computeContextSize) {
249
- contextSize = this.computeContextSize({
250
- inputTokens: inputTok,
251
- outputTokens: outputTok,
252
- cacheWriteTokens: cacheWrite,
253
- cacheReadTokens: cacheRead,
254
- });
255
- }
256
- else {
257
- contextSize = inputTok + cacheWrite + cacheRead;
258
- }
259
- // Compaction detection
260
- if (this.resolvePendingHeuristicCompaction(contextSize)) {
261
- // The drop belongs to the explicit compaction marker just recorded.
262
- }
263
- else if (this.previousContextSize > 0 &&
264
- contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
265
- this.compactionEvents.push({
266
- timestamp: new Date(event.timestamp),
267
- contextBefore: this.previousContextSize,
268
- contextAfter: contextSize,
269
- tokensReclaimed: this.previousContextSize - contextSize,
270
- source: 'heuristic',
271
- });
272
- }
273
- this.previousContextSize = contextSize;
274
- this.currentContextSize = contextSize;
275
- // Per-model usage. If the provider reported a cost, use it; otherwise
276
- // resolve pricing from the catalog (null = unpriced → priced: false,
277
- // UI renders "—").
278
- if (event.model) {
279
- const model = event.model;
280
- const pricing = event.cost ? null : (0, modelInfo_1.getModelPricing)(model);
281
- const computed = event.cost
282
- ? event.cost
283
- : pricing
284
- ? (0, modelInfo_1.calculateCostWithPricing)({
285
- inputTokens: inputTok,
286
- outputTokens: outputTok,
287
- cacheWriteTokens: cacheWrite,
288
- cacheReadTokens: cacheRead,
289
- }, pricing)
290
- : 0;
291
- const evPriced = event.cost != null || pricing != null;
292
- const acc = this.modelUsage.get(model) ?? {
293
- calls: 0,
294
- tokens: 0,
295
- inputTokens: 0,
296
- outputTokens: 0,
297
- cacheWriteTokens: 0,
298
- cacheReadTokens: 0,
299
- cost: 0,
300
- priced: true,
301
- };
302
- acc.calls++;
303
- acc.tokens += inputTok + outputTok;
304
- acc.inputTokens += inputTok;
305
- acc.outputTokens += outputTok;
306
- acc.cacheWriteTokens += cacheWrite;
307
- acc.cacheReadTokens += cacheRead;
308
- acc.cost += computed;
309
- if (!evPriced)
310
- acc.priced = false;
311
- this.modelUsage.set(model, acc);
312
- }
249
+ const normalized = (0, usageNormalization_1.normalizeProviderUsage)({
250
+ semantics: 'sidekick',
251
+ provider: event.providerId,
252
+ source: 'follow-event',
253
+ model: event.model,
254
+ inputTokens: event.tokens.input,
255
+ outputTokens: event.tokens.output,
256
+ cacheReadTokens: event.cacheTokens?.read,
257
+ cacheWriteTokens: event.cacheTokens?.write,
258
+ reasoningIncludedInOutput: false,
259
+ reportedCostUsd: event.cost,
260
+ });
261
+ const cost = this.accumulateUsage(normalized, event.timestamp, event.model);
262
+ this.attributePlanUsage(normalized.totalTokens, cost);
313
263
  }
314
- // Cost accumulation
315
- if (event.cost) {
264
+ else if (typeof event.cost === 'number' && Number.isFinite(event.cost) && event.cost >= 0) {
265
+ // Compatibility for cost-only follow events.
316
266
  this.reportedCost += event.cost;
317
267
  }
318
- // Burn rate sampling
319
- this.updateBurnRate(event.timestamp);
320
268
  // 4. Tool tracking from FollowEvent
321
269
  if (event.type === 'tool_use' && event.toolName) {
322
270
  this.recordFollowToolUse(event);
@@ -346,7 +294,7 @@ class EventAggregator {
346
294
  // 12. Gonzo/Lazyjournal analytics
347
295
  this.feedAnalytics(event.timestamp, event.summary, event.toolName);
348
296
  // Message count (skip system events)
349
- if (event.type !== 'system') {
297
+ if (event.type === 'user' || event.type === 'assistant') {
350
298
  this.messageCount++;
351
299
  }
352
300
  }
@@ -359,6 +307,8 @@ class EventAggregator {
359
307
  outputTokens: this.outputTokens,
360
308
  cacheWriteTokens: this.cacheWriteTokens,
361
309
  cacheReadTokens: this.cacheReadTokens,
310
+ reasoningTokens: this.reasoningTokens,
311
+ totalTokens: this.totalTokens,
362
312
  reportedCost: this.reportedCost,
363
313
  };
364
314
  }
@@ -475,6 +425,8 @@ class EventAggregator {
475
425
  this.outputTokens = 0;
476
426
  this.cacheWriteTokens = 0;
477
427
  this.cacheReadTokens = 0;
428
+ this.reasoningTokens = 0;
429
+ this.totalTokens = 0;
478
430
  this.reportedCost = 0;
479
431
  this.modelUsage.clear();
480
432
  this.currentContextSize = 0;
@@ -543,12 +495,14 @@ class EventAggregator {
543
495
  /** Serializes all mutable state to a JSON-safe object for snapshot persistence. */
544
496
  serialize() {
545
497
  return {
546
- version: SNAPSHOT_SCHEMA_VERSION,
498
+ version: exports.SNAPSHOT_SCHEMA_VERSION,
547
499
  tokens: {
548
500
  input: this.inputTokens,
549
501
  output: this.outputTokens,
550
502
  cacheWrite: this.cacheWriteTokens,
551
503
  cacheRead: this.cacheReadTokens,
504
+ reasoning: this.reasoningTokens,
505
+ total: this.totalTokens,
552
506
  reportedCost: this.reportedCost,
553
507
  },
554
508
  modelUsage: Array.from(this.modelUsage.entries()),
@@ -587,13 +541,15 @@ class EventAggregator {
587
541
  }
588
542
  /** Restores mutable state from a serialized snapshot. Clears transient state (pending calls). */
589
543
  restore(state) {
590
- if (state.version !== SNAPSHOT_SCHEMA_VERSION) {
591
- return; // Incompatible snapshot — caller should fall back to full replay
544
+ if (state.version !== exports.SNAPSHOT_SCHEMA_VERSION) {
545
+ return false;
592
546
  }
593
547
  this.inputTokens = state.tokens.input;
594
548
  this.outputTokens = state.tokens.output;
595
549
  this.cacheWriteTokens = state.tokens.cacheWrite;
596
550
  this.cacheReadTokens = state.tokens.cacheRead;
551
+ this.reasoningTokens = state.tokens.reasoning;
552
+ this.totalTokens = state.tokens.total;
597
553
  this.reportedCost = state.tokens.reportedCost;
598
554
  this.modelUsage = new Map(state.modelUsage);
599
555
  this.currentContextSize = state.contextSize;
@@ -617,7 +573,9 @@ class EventAggregator {
617
573
  this.permissionModeHistory = state.permissionModeHistory
618
574
  ? [...state.permissionModeHistory]
619
575
  : [];
620
- this.contextTimeline = state.contextTimeline ? [...state.contextTimeline] : [];
576
+ this.contextTimeline = state.contextTimeline
577
+ ? state.contextTimeline.slice(-this.contextTimelineCap)
578
+ : [];
621
579
  this.contextTurnIndex = state.contextTurnIndex ?? 0;
622
580
  this.timeline = [...state.timeline];
623
581
  this.messageCount = state.messageCount;
@@ -640,46 +598,30 @@ class EventAggregator {
640
598
  this.pendingSubagents.clear();
641
599
  this.pendingUserRequest = null;
642
600
  this.planExtractor.reset();
601
+ return true;
643
602
  }
644
603
  // ═══════════════════════════════════════════════════════════════════════
645
604
  // Private: Token & Context
646
605
  // ═══════════════════════════════════════════════════════════════════════
647
606
  accumulateUsage(usage, timestamp, model) {
648
- const inputTok = usage.input_tokens;
649
- const outputTok = usage.output_tokens;
650
- const cacheWrite = usage.cache_creation_input_tokens ?? 0;
651
- const cacheRead = usage.cache_read_input_tokens ?? 0;
652
- const reasoningTok = usage.reasoning_tokens ?? 0;
653
- const reported = usage.reported_cost ?? 0;
654
- // Resolve provider pricing when we have a model ID. Lets sessions with no
655
- // reported_cost (Claude Code, Codex) still show real dollar figures.
607
+ const inputTok = usage.uncachedInputTokens;
608
+ const outputTok = usage.outputTokens;
609
+ const cacheWrite = usage.cacheWriteTokens;
610
+ const cacheRead = usage.cacheReadTokens;
611
+ const reasoningTok = usage.reasoningTokens;
656
612
  const modelKey = model ?? this.currentModel;
657
- const pricing = modelKey ? (0, modelInfo_1.getModelPricing)(modelKey) : null;
658
- let priced = reported > 0 || pricing !== null;
659
- let cost;
660
- if (reported > 0) {
661
- cost = reported;
662
- }
663
- else if (pricing) {
664
- cost = (0, modelInfo_1.calculateCostWithPricing)({
665
- inputTokens: inputTok,
666
- outputTokens: outputTok,
667
- cacheWriteTokens: cacheWrite,
668
- cacheReadTokens: cacheRead,
669
- reasoningTokens: reasoningTok,
670
- }, pricing);
671
- }
672
- else {
673
- cost = 0;
674
- priced = false;
675
- }
613
+ const pricedUsage = (0, usageNormalization_1.calculateNormalizedUsageCost)({ usage, modelId: modelKey ?? undefined });
614
+ const priced = pricedUsage.source !== 'unpriced';
615
+ const cost = pricedUsage.costUsd ?? 0;
676
616
  this.inputTokens += inputTok;
677
617
  this.outputTokens += outputTok;
678
618
  this.cacheWriteTokens += cacheWrite;
679
619
  this.cacheReadTokens += cacheRead;
620
+ this.reasoningTokens += reasoningTok;
621
+ this.totalTokens += usage.totalTokens;
680
622
  this.reportedCost += cost;
681
623
  // Burn rate sample accumulation
682
- this.tokensSinceLastSample += inputTok + outputTok;
624
+ this.tokensSinceLastSample += usage.totalTokens;
683
625
  // Context size computation
684
626
  let contextSize;
685
627
  if (this.computeContextSize) {
@@ -688,7 +630,7 @@ class EventAggregator {
688
630
  outputTokens: outputTok,
689
631
  cacheWriteTokens: cacheWrite,
690
632
  cacheReadTokens: cacheRead,
691
- reasoningTokens: usage.reasoning_tokens,
633
+ reasoningTokens: reasoningTok,
692
634
  });
693
635
  }
694
636
  else {
@@ -716,6 +658,9 @@ class EventAggregator {
716
658
  inputTokens: contextSize,
717
659
  turnIndex: this.contextTurnIndex++,
718
660
  });
661
+ if (this.contextTimeline.length > this.contextTimelineCap) {
662
+ this.contextTimeline.splice(0, this.contextTimeline.length - this.contextTimelineCap);
663
+ }
719
664
  // Per-model usage. Accumulate reasoning tokens and an inverted-sticky
720
665
  // `priced` flag so one unpriced event taints the aggregate (UI renders "—").
721
666
  const perModelKey = model ?? this.currentModel ?? 'unknown';
@@ -731,7 +676,7 @@ class EventAggregator {
731
676
  priced: true,
732
677
  };
733
678
  acc.calls++;
734
- acc.tokens += inputTok + outputTok;
679
+ acc.tokens += usage.totalTokens;
735
680
  acc.inputTokens += inputTok;
736
681
  acc.outputTokens += outputTok;
737
682
  acc.cacheWriteTokens += cacheWrite;
@@ -808,7 +753,7 @@ class EventAggregator {
808
753
  // Private: Tool Extraction from SessionEvent content blocks
809
754
  // ═══════════════════════════════════════════════════════════════════════
810
755
  extractToolsFromContent(event) {
811
- const content = event.message.content;
756
+ const content = event.message?.content;
812
757
  if (!Array.isArray(content))
813
758
  return;
814
759
  for (const block of content) {
@@ -961,7 +906,7 @@ class EventAggregator {
961
906
  this.finishReasons.set(reason, (this.finishReasons.get(reason) ?? 0) + 1);
962
907
  }
963
908
  if (metadata.providerError) {
964
- this.recordError('ProviderAPI', (0, errorTaxonomy_1.categorizeError)(metadata.providerError, metadata.providerError.type), event.timestamp, event.message.model ?? this.currentModel ?? 'unknown');
909
+ this.recordError('ProviderAPI', (0, errorTaxonomy_1.categorizeError)(metadata.providerError, metadata.providerError.type), event.timestamp, event.message?.model ?? this.currentModel ?? 'unknown');
965
910
  }
966
911
  }
967
912
  recordError(tool, category, timestamp, model) {
@@ -1065,7 +1010,7 @@ class EventAggregator {
1065
1010
  // Private: Task State from SessionEvent
1066
1011
  // ═══════════════════════════════════════════════════════════════════════
1067
1012
  extractTaskStateFromEvent(event) {
1068
- const content = event.message.content;
1013
+ const content = event.message?.content;
1069
1014
  if (!Array.isArray(content))
1070
1015
  return;
1071
1016
  for (const block of content) {
@@ -1480,7 +1425,7 @@ class EventAggregator {
1480
1425
  // Private: Subagent Tracking from SessionEvent
1481
1426
  // ═══════════════════════════════════════════════════════════════════════
1482
1427
  extractSubagentFromEvent(event) {
1483
- const content = event.message.content;
1428
+ const content = event.message?.content;
1484
1429
  if (!Array.isArray(content))
1485
1430
  return;
1486
1431
  for (const block of content) {
@@ -1646,7 +1591,7 @@ class EventAggregator {
1646
1591
  // Private: Context Attribution from SessionEvent
1647
1592
  // ═══════════════════════════════════════════════════════════════════════
1648
1593
  attributeContextFromEvent(event) {
1649
- const content = event.message.content;
1594
+ const content = event.message?.content;
1650
1595
  if (event.type === 'user') {
1651
1596
  if (Array.isArray(content)) {
1652
1597
  for (const block of content) {
@@ -1853,7 +1798,8 @@ class EventAggregator {
1853
1798
  break;
1854
1799
  case 'system':
1855
1800
  tlType = 'session_start';
1856
- description = this.extractTextContent(event) ?? event.message.sourceLabel ?? 'System event';
1801
+ description =
1802
+ this.extractTextContent(event) ?? event.message?.sourceLabel ?? 'System event';
1857
1803
  noiseLevel = 'system';
1858
1804
  break;
1859
1805
  default:
@@ -1945,10 +1891,10 @@ class EventAggregator {
1945
1891
  // Private: Helper methods
1946
1892
  // ═══════════════════════════════════════════════════════════════════════
1947
1893
  estimateTokens(text) {
1948
- return Math.ceil(text.length / 4);
1894
+ return (0, tokenEstimation_1.estimateTextTokens)(text).count;
1949
1895
  }
1950
1896
  hasTextContent(event) {
1951
- const content = event.message.content;
1897
+ const content = event.message?.content;
1952
1898
  if (typeof content === 'string')
1953
1899
  return content.length > 0;
1954
1900
  if (Array.isArray(content)) {
@@ -1957,7 +1903,7 @@ class EventAggregator {
1957
1903
  return false;
1958
1904
  }
1959
1905
  extractTextContent(event) {
1960
- const content = event.message.content;
1906
+ const content = event.message?.content;
1961
1907
  if (typeof content === 'string')
1962
1908
  return content;
1963
1909
  if (Array.isArray(content)) {
@@ -1986,7 +1932,7 @@ class EventAggregator {
1986
1932
  // Private: Gonzo/Lazyjournal analytics
1987
1933
  // ═══════════════════════════════════════════════════════════════════════
1988
1934
  extractToolNameFromEvent(event) {
1989
- const content = event.message.content;
1935
+ const content = event.message?.content;
1990
1936
  if (!Array.isArray(content))
1991
1937
  return undefined;
1992
1938
  for (const block of content) {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module aggregation/snapshot
9
9
  */
10
- import type { SerializedAggregatorState } from './EventAggregator';
10
+ import { type SerializedAggregatorState } from './EventAggregator';
11
11
  /** Snapshot sidecar file content. */
12
12
  export interface SessionSnapshot {
13
13
  /** Schema version for forward-compatibility checks. */
@@ -50,8 +50,9 @@ exports.isSnapshotValid = isSnapshotValid;
50
50
  const fs = __importStar(require("fs"));
51
51
  const path = __importStar(require("path"));
52
52
  const paths_1 = require("../paths");
53
+ const EventAggregator_1 = require("./EventAggregator");
53
54
  /** Current schema version — bump when format changes. */
54
- const SNAPSHOT_VERSION = 3;
55
+ const SNAPSHOT_VERSION = EventAggregator_1.SNAPSHOT_SCHEMA_VERSION;
55
56
  /** Returns the directory for snapshot files. */
56
57
  function getSnapshotsDir() {
57
58
  return path.join((0, paths_1.getConfigDir)(), 'snapshots');
@@ -31,6 +31,8 @@ export interface EventAggregatorOptions {
31
31
  timelineCap?: number;
32
32
  /** Maximum latency records to retain (default 100). */
33
33
  latencyCap?: number;
34
+ /** Maximum context-size points to retain (default 2000). */
35
+ contextTimelineCap?: number;
34
36
  /** Burn rate sliding window in ms (default 5 * 60_000). */
35
37
  burnWindowMs?: number;
36
38
  /** Burn rate sample interval in ms (default 10_000). */
@@ -54,6 +56,9 @@ export interface AggregatedTokens {
54
56
  outputTokens: number;
55
57
  cacheWriteTokens: number;
56
58
  cacheReadTokens: number;
59
+ reasoningTokens: number;
60
+ /** Overall total after applying provider reasoning-in-output semantics. */
61
+ totalTokens: number;
57
62
  reportedCost: number;
58
63
  }
59
64
  /** Per-model usage breakdown. */
@@ -42,7 +42,7 @@ export interface AutoSwitchControllerOptions {
42
42
  cooldownMs?: number;
43
43
  now?: () => number;
44
44
  }
45
- export declare function decideAutoSwitch(_provider: AccountProviderId, active: AutoSwitchActiveAccount, candidates: AutoSwitchCandidate[], config?: AutoSwitchConfig): AutoSwitchDecision | null;
45
+ export declare function decideAutoSwitch(_provider: AccountProviderId, active: AutoSwitchActiveAccount, candidates: AutoSwitchCandidate[], config?: AutoSwitchConfig, nowMs?: number): AutoSwitchDecision | null;
46
46
  export declare class AutoSwitchController implements Disposable {
47
47
  private readonly quotaService;
48
48
  private readonly getClaudeAccounts;
@@ -11,6 +11,7 @@ exports.DEFAULT_AUTO_SWITCH_CONFIG = {
11
11
  thresholdPct: 90,
12
12
  };
13
13
  const MATERIAL_REMAINING_IMPROVEMENT_PCT = 5;
14
+ const MAX_STALE_SNAPSHOT_AGE_MS = 5 * 60 * 60 * 1000;
14
15
  function mergedConfig(config) {
15
16
  return {
16
17
  ...exports.DEFAULT_AUTO_SWITCH_CONFIG,
@@ -27,7 +28,17 @@ function quotaRemaining(quota) {
27
28
  return 0;
28
29
  return Math.max(0, 100 - quotaUtilization(quota));
29
30
  }
30
- function decideAutoSwitch(_provider, active, candidates, config) {
31
+ function isUsableCandidateQuota(quota, nowMs) {
32
+ if (!quota?.available)
33
+ return false;
34
+ if (quota.stale !== true)
35
+ return true;
36
+ const capturedAt = Date.parse(quota.capturedAt ?? '');
37
+ if (!Number.isFinite(capturedAt) || capturedAt > nowMs)
38
+ return false;
39
+ return nowMs - capturedAt <= MAX_STALE_SNAPSHOT_AGE_MS;
40
+ }
41
+ function decideAutoSwitch(_provider, active, candidates, config, nowMs = Date.now()) {
31
42
  const cfg = mergedConfig(config);
32
43
  if (!cfg.enabled)
33
44
  return null;
@@ -40,7 +51,7 @@ function decideAutoSwitch(_provider, active, candidates, config) {
40
51
  const best = candidates
41
52
  .filter((candidate) => candidate.accountId !== active.accountId &&
42
53
  candidate.switchable !== false &&
43
- candidate.quota?.available)
54
+ isUsableCandidateQuota(candidate.quota, nowMs))
44
55
  .sort((a, b) => quotaRemaining(b.quota) - quotaRemaining(a.quota))[0];
45
56
  if (!best)
46
57
  return null;
@@ -129,7 +140,7 @@ class AutoSwitchController {
129
140
  quota: accountId === activeAccountId ? activeQuota : this.readSnapshot(providerId, accountId),
130
141
  };
131
142
  });
132
- const decision = decideAutoSwitch(providerId, { accountId: activeAccountId, quota: activeQuota }, candidates, this.config);
143
+ const decision = decideAutoSwitch(providerId, { accountId: activeAccountId, quota: activeQuota }, candidates, this.config, this.now());
133
144
  if (!decision)
134
145
  return;
135
146
  if (this.switchedDuringCrossing.has(providerId))
package/dist/browser.d.ts CHANGED
@@ -8,6 +8,12 @@
8
8
  * For pricing hydration (LiteLLM catalog refresh), use `sidekick-shared/node`.
9
9
  */
10
10
  export { getModelContextWindowSize, DEFAULT_CONTEXT_WINDOW } from './modelContext';
11
+ export { normalizeProviderUsage, extractNormalizedUsage, calculateNormalizedUsageCost, } from './usageNormalization';
12
+ export type { UsageProvider, UsageSemantics, UsageNormalizationProvenance, ProviderUsageInput, AnthropicUsageInput, OpenAIUsageInput, SidekickUsageInput, NormalizedUsage, PricingProvenance, NormalizedUsageCostInput, NormalizedUsageCost, } from './usageNormalization';
13
+ export { estimateTextTokens, estimateSerializedTokens } from './tokenEstimation';
14
+ export type { ExactTokenCounterContext, ExactTokenCounter, TokenEstimationMethod, TokenEstimationConfidence, TokenEstimationOptions, TokenEstimate, } from './tokenEstimation';
15
+ export { projectSessionTranscript } from './transcript';
16
+ export type { TranscriptFidelity, TranscriptSourceProvenance, CanonicalTranscriptBlock, CanonicalTranscriptMessage, CanonicalToolCall, CanonicalTranscriptUsageTotals, CanonicalSessionTranscript, ProjectSessionTranscriptOptions, } from './transcript';
11
17
  export { formatDurationMs, formatTokenCount } from './formatting';
12
18
  export type { FormatDurationMsOptions, FormatTokenCountOptions } from './formatting';
13
19
  export { buildSessionContextSnapshot, calculateSessionContextPressure, createSessionContextProjector, } from './context/sessionContext';
package/dist/browser.js CHANGED
@@ -9,10 +9,19 @@
9
9
  * For pricing hydration (LiteLLM catalog refresh), use `sidekick-shared/node`.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.formatTokenCount = exports.formatDurationMs = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
12
+ exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.formatTokenCount = exports.formatDurationMs = exports.projectSessionTranscript = exports.estimateSerializedTokens = exports.estimateTextTokens = exports.calculateNormalizedUsageCost = exports.extractNormalizedUsage = exports.normalizeProviderUsage = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
13
13
  var modelContext_1 = require("./modelContext");
14
14
  Object.defineProperty(exports, "getModelContextWindowSize", { enumerable: true, get: function () { return modelContext_1.getModelContextWindowSize; } });
15
15
  Object.defineProperty(exports, "DEFAULT_CONTEXT_WINDOW", { enumerable: true, get: function () { return modelContext_1.DEFAULT_CONTEXT_WINDOW; } });
16
+ var usageNormalization_1 = require("./usageNormalization");
17
+ Object.defineProperty(exports, "normalizeProviderUsage", { enumerable: true, get: function () { return usageNormalization_1.normalizeProviderUsage; } });
18
+ Object.defineProperty(exports, "extractNormalizedUsage", { enumerable: true, get: function () { return usageNormalization_1.extractNormalizedUsage; } });
19
+ Object.defineProperty(exports, "calculateNormalizedUsageCost", { enumerable: true, get: function () { return usageNormalization_1.calculateNormalizedUsageCost; } });
20
+ var tokenEstimation_1 = require("./tokenEstimation");
21
+ Object.defineProperty(exports, "estimateTextTokens", { enumerable: true, get: function () { return tokenEstimation_1.estimateTextTokens; } });
22
+ Object.defineProperty(exports, "estimateSerializedTokens", { enumerable: true, get: function () { return tokenEstimation_1.estimateSerializedTokens; } });
23
+ var transcript_1 = require("./transcript");
24
+ Object.defineProperty(exports, "projectSessionTranscript", { enumerable: true, get: function () { return transcript_1.projectSessionTranscript; } });
16
25
  var formatting_1 = require("./formatting");
17
26
  Object.defineProperty(exports, "formatDurationMs", { enumerable: true, get: function () { return formatting_1.formatDurationMs; } });
18
27
  Object.defineProperty(exports, "formatTokenCount", { enumerable: true, get: function () { return formatting_1.formatTokenCount; } });
@@ -32,6 +32,8 @@ export interface CodexQuotaApiOptions {
32
32
  usageUrl?: string;
33
33
  resetCreditsUrl?: string;
34
34
  capturedAt?: string;
35
+ signal?: AbortSignal;
36
+ timeoutMs?: number;
35
37
  }
36
38
  export declare function quotaFromCodexRateLimits(rateLimits: CodexRateLimits | null | undefined, source?: 'api' | 'session' | 'cache', capturedAt?: string): QuotaState | null;
37
39
  export declare function readLatestCodexQuotaFromRollouts(sessionPaths: string[], options?: {
@@ -414,10 +414,19 @@ async function fetchCodexQuotaFromApi(options = {}) {
414
414
  sevenDayLabel: 'Secondary',
415
415
  };
416
416
  }
417
+ const controller = new AbortController();
418
+ const onAbort = () => controller.abort();
419
+ if (options.signal?.aborted)
420
+ controller.abort();
421
+ else
422
+ options.signal?.addEventListener('abort', onAbort, { once: true });
423
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
424
+ timeout.unref();
417
425
  try {
418
426
  const fetchImpl = options.fetchImpl ?? fetch;
419
427
  const response = await fetchImpl(options.usageUrl ?? CHATGPT_USAGE_URL, {
420
428
  method: 'GET',
429
+ signal: controller.signal,
421
430
  headers: {
422
431
  Authorization: `Bearer ${accessToken}`,
423
432
  Accept: 'application/json',
@@ -468,6 +477,7 @@ async function fetchCodexQuotaFromApi(options = {}) {
468
477
  accessToken,
469
478
  accountId: auth?.accountId,
470
479
  capturedAt,
480
+ signal: controller.signal,
471
481
  });
472
482
  if (resetCredits) {
473
483
  quota.resetCredits = resetCredits;
@@ -488,6 +498,10 @@ async function fetchCodexQuotaFromApi(options = {}) {
488
498
  sevenDayLabel: 'Secondary',
489
499
  };
490
500
  }
501
+ finally {
502
+ clearTimeout(timeout);
503
+ options.signal?.removeEventListener('abort', onAbort);
504
+ }
491
505
  }
492
506
  async function fetchCodexResetCreditsFromApi(options = {}) {
493
507
  const capturedAt = options.capturedAt ?? new Date().toISOString();
@@ -497,6 +511,14 @@ async function fetchCodexResetCreditsFromApi(options = {}) {
497
511
  const accessToken = auth?.accessToken;
498
512
  if (!accessToken)
499
513
  return undefined;
514
+ const controller = new AbortController();
515
+ const onAbort = () => controller.abort();
516
+ if (options.signal?.aborted)
517
+ controller.abort();
518
+ else
519
+ options.signal?.addEventListener('abort', onAbort, { once: true });
520
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
521
+ timeout.unref();
500
522
  try {
501
523
  const fetchImpl = options.fetchImpl ?? fetch;
502
524
  const headers = {
@@ -511,6 +533,7 @@ async function fetchCodexResetCreditsFromApi(options = {}) {
511
533
  const response = await fetchImpl(options.resetCreditsUrl ?? CHATGPT_RESET_CREDITS_URL, {
512
534
  method: 'GET',
513
535
  headers,
536
+ signal: controller.signal,
514
537
  });
515
538
  if (!response.ok)
516
539
  return undefined;
@@ -519,6 +542,10 @@ async function fetchCodexResetCreditsFromApi(options = {}) {
519
542
  catch {
520
543
  return undefined;
521
544
  }
545
+ finally {
546
+ clearTimeout(timeout);
547
+ options.signal?.removeEventListener('abort', onAbort);
548
+ }
522
549
  }
523
550
  function readCodexAuth(codexHome) {
524
551
  try {