sidekick-shared 0.25.0 → 0.26.1

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 (119) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +50 -3
  3. package/dist/accountChanges.js +22 -3
  4. package/dist/accountRegistry.d.ts +22 -1
  5. package/dist/accountRegistry.js +37 -14
  6. package/dist/accountStatus.d.ts +9 -1
  7. package/dist/accountStatus.js +3 -3
  8. package/dist/accounts.d.ts +3 -1
  9. package/dist/accounts.js +84 -64
  10. package/dist/aggregation/EventAggregator.d.ts +15 -2
  11. package/dist/aggregation/EventAggregator.js +54 -9
  12. package/dist/aggregation/costProvenance.d.ts +14 -0
  13. package/dist/aggregation/costProvenance.js +35 -0
  14. package/dist/aggregation/snapshot.d.ts +13 -2
  15. package/dist/aggregation/snapshot.js +57 -6
  16. package/dist/aggregation/types.d.ts +30 -1
  17. package/dist/analytics/compactionLedger.js +15 -5
  18. package/dist/analytics/qualityScore.js +2 -1
  19. package/dist/browser.d.ts +11 -1
  20. package/dist/browser.js +29 -1
  21. package/dist/codexProfiles.d.ts +3 -1
  22. package/dist/codexProfiles.js +4 -2
  23. package/dist/codexQuota.d.ts +2 -0
  24. package/dist/codexQuota.js +11 -66
  25. package/dist/failingTools.d.ts +37 -0
  26. package/dist/failingTools.js +70 -0
  27. package/dist/formatters/sessionDump.js +13 -7
  28. package/dist/formatting.d.ts +13 -0
  29. package/dist/formatting.js +35 -0
  30. package/dist/historicalStore.d.ts +47 -0
  31. package/dist/historicalStore.js +312 -0
  32. package/dist/index.d.ts +46 -12
  33. package/dist/index.js +112 -24
  34. package/dist/modelContext.js +8 -3
  35. package/dist/modelInfo.js +45 -27
  36. package/dist/node.d.ts +19 -0
  37. package/dist/node.js +47 -1
  38. package/dist/observedContextWindows.d.ts +7 -0
  39. package/dist/observedContextWindows.js +17 -2
  40. package/dist/observedSessionCollector.d.ts +3 -8
  41. package/dist/observedSessionCollector.js +10 -27
  42. package/dist/parsers/sessionActivityDetector.d.ts +24 -2
  43. package/dist/parsers/sessionActivityDetector.js +95 -17
  44. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  45. package/dist/parsers/sessionPathResolver.js +13 -10
  46. package/dist/peakHours.d.ts +34 -3
  47. package/dist/peakHours.js +92 -7
  48. package/dist/pricingCatalog.d.ts +7 -0
  49. package/dist/pricingCatalog.js +8 -5
  50. package/dist/providers/claudeCode.js +11 -96
  51. package/dist/providers/codex.d.ts +2 -0
  52. package/dist/providers/codex.js +94 -259
  53. package/dist/providers/detect.d.ts +2 -0
  54. package/dist/providers/detect.js +29 -24
  55. package/dist/providers/factory.d.ts +6 -0
  56. package/dist/providers/openCode.d.ts +2 -0
  57. package/dist/providers/openCode.js +105 -121
  58. package/dist/providers/rolloutWalker.d.ts +53 -0
  59. package/dist/providers/rolloutWalker.js +209 -0
  60. package/dist/providers/types.d.ts +30 -0
  61. package/dist/quota.d.ts +30 -3
  62. package/dist/quota.js +35 -3
  63. package/dist/quotaHistory.d.ts +1 -1
  64. package/dist/quotaHistory.js +36 -35
  65. package/dist/quotaPresentation.js +17 -5
  66. package/dist/quotaResolver.d.ts +83 -0
  67. package/dist/quotaResolver.js +234 -0
  68. package/dist/quotaSnapshots.d.ts +15 -2
  69. package/dist/quotaSnapshots.js +25 -3
  70. package/dist/quotaThresholds.d.ts +60 -0
  71. package/dist/quotaThresholds.js +90 -0
  72. package/dist/report/htmlReportGenerator.js +10 -4
  73. package/dist/report/sessionReportInputs.d.ts +23 -0
  74. package/dist/report/sessionReportInputs.js +34 -0
  75. package/dist/schemas/index.d.ts +2 -0
  76. package/dist/schemas/index.js +10 -1
  77. package/dist/schemas/quota.d.ts +139 -0
  78. package/dist/schemas/quota.js +17 -2
  79. package/dist/schemas/quotaHistory.d.ts +1 -0
  80. package/dist/schemas/quotaHistory.js +1 -1
  81. package/dist/schemas/sessionEvent.d.ts +6 -0
  82. package/dist/schemas/sessionEvent.js +1 -0
  83. package/dist/schemas/stateFile.d.ts +198 -0
  84. package/dist/schemas/stateFile.js +77 -0
  85. package/dist/schemas/transcript.js +2 -0
  86. package/dist/sessionFingerprint.d.ts +25 -0
  87. package/dist/sessionFingerprint.js +46 -0
  88. package/dist/sessionPreviews.d.ts +7 -1
  89. package/dist/sessionPreviews.js +15 -3
  90. package/dist/sessionStats.d.ts +51 -0
  91. package/dist/sessionStats.js +166 -0
  92. package/dist/stateFile.d.ts +113 -0
  93. package/dist/stateFile.js +160 -0
  94. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  95. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  96. package/dist/statusline/formatter.d.ts +13 -0
  97. package/dist/statusline/formatter.js +48 -6
  98. package/dist/statusline/index.d.ts +10 -2
  99. package/dist/statusline/index.js +19 -1
  100. package/dist/tokenSummary.d.ts +62 -0
  101. package/dist/tokenSummary.js +72 -0
  102. package/dist/transcript.d.ts +5 -0
  103. package/dist/transcript.js +2 -0
  104. package/dist/types/observedSessionV1.d.ts +2 -0
  105. package/dist/types/observedSessionV1.js +75 -18
  106. package/dist/types/sessionEvent.d.ts +2 -0
  107. package/dist/usage/billingBlocks.d.ts +85 -0
  108. package/dist/usage/billingBlocks.js +153 -0
  109. package/dist/usage/importSessionHistory.d.ts +53 -0
  110. package/dist/usage/importSessionHistory.js +88 -0
  111. package/dist/usage/usageEvents.d.ts +92 -0
  112. package/dist/usage/usageEvents.js +322 -0
  113. package/dist/usage/usageReports.d.ts +76 -0
  114. package/dist/usage/usageReports.js +208 -0
  115. package/dist/watchers/factory.d.ts +6 -0
  116. package/dist/watchers/factory.js +30 -25
  117. package/dist/zaiQuotaApi.d.ts +2 -0
  118. package/dist/zaiQuotaApi.js +2 -0
  119. package/package.json +1 -1
@@ -1,9 +1,15 @@
1
1
  import type { ActiveAccountStatus } from '../accountStatus';
2
2
  import type { QuotaState } from '../quota';
3
+ import type { ClaudeStatuslinePayload } from './claudeStatuslinePayload';
3
4
  export interface StatuslineInput {
4
5
  accounts: ActiveAccountStatus;
5
6
  claudeQuota?: QuotaState | null;
6
7
  codexQuota?: QuotaState | null;
8
+ /**
9
+ * The document Claude Code piped to the status-line command, when running
10
+ * as one. Adds context usage, session cost, and prompt-cache hit rate.
11
+ */
12
+ live?: ClaudeStatuslinePayload | null;
7
13
  now?: Date;
8
14
  }
9
15
  export interface StatuslineSelection {
@@ -13,4 +19,11 @@ export interface StatuslineSelection {
13
19
  quota: QuotaState | null;
14
20
  }
15
21
  export declare function selectStatuslineAccount(input: StatuslineInput): StatuslineSelection | null;
22
+ /**
23
+ * One-line status. Segments, in order, each omitted when unknown:
24
+ * `acct:<label> · 5h 42% resets 14:00 · ~1h20m left · 7d 61% · (2h ago) · ctx 37% · $0.42 · cache 92%`
25
+ *
26
+ * The age segment appears only for cached quota older than five minutes, so
27
+ * a snapshot from yesterday can no longer read like a live figure.
28
+ */
16
29
  export declare function formatStatusline(input: StatuslineInput): string;
@@ -23,20 +23,62 @@ function selectStatuslineAccount(input) {
23
23
  }
24
24
  return null;
25
25
  }
26
+ /**
27
+ * One-line status. Segments, in order, each omitted when unknown:
28
+ * `acct:<label> · 5h 42% resets 14:00 · ~1h20m left · 7d 61% · (2h ago) · ctx 37% · $0.42 · cache 92%`
29
+ *
30
+ * The age segment appears only for cached quota older than five minutes, so
31
+ * a snapshot from yesterday can no longer read like a live figure.
32
+ */
26
33
  function formatStatusline(input) {
27
34
  const selected = selectStatuslineAccount(input);
35
+ const now = input.now ?? new Date();
36
+ const liveSegments = formatLiveSegments(input.live);
28
37
  if (!selected)
29
- return 'acct:none · quota unavailable';
38
+ return joinSegments(['acct:none', 'quota unavailable', ...liveSegments]);
30
39
  const account = compactLabel(selected.accountLabel);
31
40
  const quota = selected.quota;
32
- if (!quota?.available)
33
- return `acct:${account} · quota unavailable`;
34
- const now = input.now ?? new Date();
41
+ if (!quota?.available) {
42
+ return joinSegments([`acct:${account}`, 'quota unavailable', ...liveSegments]);
43
+ }
35
44
  const utilization = clampPercent(quota.fiveHour.utilization);
36
45
  const reset = formatReset(quota.fiveHour.resetsAt, now);
37
46
  const eta = estimateWindowEta(quota, now);
38
- const etaText = eta == null ? '' : ` · ~${formatMinutes(eta)} left`;
39
- return `acct:${account} · 5h ${Math.round(utilization)}% resets ${reset}${etaText}`;
47
+ const segments = [`acct:${account}`, `5h ${Math.round(utilization)}% resets ${reset}`];
48
+ if (eta != null)
49
+ segments.push(`~${formatMinutes(eta)} left`);
50
+ if (quota.sevenDay.resetsAt && quota.sevenDay.utilization > 0) {
51
+ segments.push(`7d ${Math.round(clampPercent(quota.sevenDay.utilization))}%`);
52
+ }
53
+ if (quota.freshness && quota.freshness !== 'fresh') {
54
+ segments.push(`(${(0, quota_1.formatQuotaAge)(quota.ageMs)})`);
55
+ }
56
+ return joinSegments([...segments, ...liveSegments]);
57
+ }
58
+ function formatLiveSegments(live) {
59
+ if (!live)
60
+ return [];
61
+ const segments = [];
62
+ const context = live.contextWindow?.usedPercentage;
63
+ if (typeof context === 'number')
64
+ segments.push(`ctx ${Math.round(clampPercent(context))}%`);
65
+ const cost = live.cost?.totalCostUsd;
66
+ if (typeof cost === 'number' && cost > 0)
67
+ segments.push(`$${formatCost(cost)}`);
68
+ const hitRatio = live.promptCache?.hitRatio;
69
+ if (typeof hitRatio === 'number')
70
+ segments.push(`cache ${Math.round(clampPercent(hitRatio * 100))}%`);
71
+ return segments;
72
+ }
73
+ function joinSegments(segments) {
74
+ return segments.join(' · ');
75
+ }
76
+ function formatCost(value) {
77
+ if (value >= 100)
78
+ return value.toFixed(0);
79
+ if (value >= 10)
80
+ return value.toFixed(1);
81
+ return value.toFixed(2);
40
82
  }
41
83
  function estimateWindowEta(quota, now) {
42
84
  const resetAt = Date.parse(quota.fiveHour.resetsAt);
@@ -1,7 +1,15 @@
1
1
  export { getActiveAccountStatus } from '../accountStatus';
2
- export type { ActiveAccountStatus, ActiveProviderAccountStatus } from '../accountStatus';
3
- export { readQuotaSnapshot } from '../quotaSnapshots';
2
+ export type { ActiveAccountStatus, ActiveAccountStatusOptions, ActiveProviderAccountStatus, } from '../accountStatus';
3
+ export { readQuotaSnapshot, writeQuotaSnapshot } from '../quotaSnapshots';
4
4
  export type { QuotaSnapshotProviderId } from '../quotaSnapshots';
5
+ export { appendQuotaHistorySample, getWorkspaceIdFromPath } from '../quotaHistory';
6
+ export type { QuotaHistorySample } from '../quotaHistory';
7
+ export { classifyQuotaFreshness, formatQuotaAge } from '../quota';
8
+ export type { QuotaFreshness, QuotaState } from '../quota';
5
9
  export { BurnRateCalculator, estimateTimeToQuota } from './BurnRateCalculator';
6
10
  export { formatStatusline, selectStatuslineAccount } from './formatter';
7
11
  export type { StatuslineInput, StatuslineSelection } from './formatter';
12
+ export { parseClaudeStatuslinePayload, quotaFromStatuslinePayload, } from './claudeStatuslinePayload';
13
+ export type { ClaudeStatuslinePayload, ClaudeStatuslineRateLimitWindow, QuotaFromStatuslineOptions, } from './claudeStatuslinePayload';
14
+ export { STATE_FILE_NAME, STATE_FILE_SCHEMA_VERSION, billingBlockToStateFile, getStateFilePath, quotaToStateFile, readStateFile, writeStateFile, } from '../stateFile';
15
+ export type { SidekickStateFile, SidekickStateInput, StateFileAccount, StateFileBillingBlock, StateFileContext, StateFileQuota, StateFileQuotaWindow, StateFileSession, StateFileWriter, WriteStateFileOptions, } from '../stateFile';
@@ -1,13 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.readQuotaSnapshot = exports.getActiveAccountStatus = void 0;
3
+ exports.writeStateFile = exports.readStateFile = exports.quotaToStateFile = exports.getStateFilePath = exports.billingBlockToStateFile = exports.STATE_FILE_SCHEMA_VERSION = exports.STATE_FILE_NAME = exports.quotaFromStatuslinePayload = exports.parseClaudeStatuslinePayload = exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.formatQuotaAge = exports.classifyQuotaFreshness = exports.getWorkspaceIdFromPath = exports.appendQuotaHistorySample = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.getActiveAccountStatus = void 0;
4
4
  var accountStatus_1 = require("../accountStatus");
5
5
  Object.defineProperty(exports, "getActiveAccountStatus", { enumerable: true, get: function () { return accountStatus_1.getActiveAccountStatus; } });
6
6
  var quotaSnapshots_1 = require("../quotaSnapshots");
7
7
  Object.defineProperty(exports, "readQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.readQuotaSnapshot; } });
8
+ Object.defineProperty(exports, "writeQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.writeQuotaSnapshot; } });
9
+ var quotaHistory_1 = require("../quotaHistory");
10
+ Object.defineProperty(exports, "appendQuotaHistorySample", { enumerable: true, get: function () { return quotaHistory_1.appendQuotaHistorySample; } });
11
+ Object.defineProperty(exports, "getWorkspaceIdFromPath", { enumerable: true, get: function () { return quotaHistory_1.getWorkspaceIdFromPath; } });
12
+ var quota_1 = require("../quota");
13
+ Object.defineProperty(exports, "classifyQuotaFreshness", { enumerable: true, get: function () { return quota_1.classifyQuotaFreshness; } });
14
+ Object.defineProperty(exports, "formatQuotaAge", { enumerable: true, get: function () { return quota_1.formatQuotaAge; } });
8
15
  var BurnRateCalculator_1 = require("./BurnRateCalculator");
9
16
  Object.defineProperty(exports, "BurnRateCalculator", { enumerable: true, get: function () { return BurnRateCalculator_1.BurnRateCalculator; } });
10
17
  Object.defineProperty(exports, "estimateTimeToQuota", { enumerable: true, get: function () { return BurnRateCalculator_1.estimateTimeToQuota; } });
11
18
  var formatter_1 = require("./formatter");
12
19
  Object.defineProperty(exports, "formatStatusline", { enumerable: true, get: function () { return formatter_1.formatStatusline; } });
13
20
  Object.defineProperty(exports, "selectStatuslineAccount", { enumerable: true, get: function () { return formatter_1.selectStatuslineAccount; } });
21
+ var claudeStatuslinePayload_1 = require("./claudeStatuslinePayload");
22
+ Object.defineProperty(exports, "parseClaudeStatuslinePayload", { enumerable: true, get: function () { return claudeStatuslinePayload_1.parseClaudeStatuslinePayload; } });
23
+ Object.defineProperty(exports, "quotaFromStatuslinePayload", { enumerable: true, get: function () { return claudeStatuslinePayload_1.quotaFromStatuslinePayload; } });
24
+ var stateFile_1 = require("../stateFile");
25
+ Object.defineProperty(exports, "STATE_FILE_NAME", { enumerable: true, get: function () { return stateFile_1.STATE_FILE_NAME; } });
26
+ Object.defineProperty(exports, "STATE_FILE_SCHEMA_VERSION", { enumerable: true, get: function () { return stateFile_1.STATE_FILE_SCHEMA_VERSION; } });
27
+ Object.defineProperty(exports, "billingBlockToStateFile", { enumerable: true, get: function () { return stateFile_1.billingBlockToStateFile; } });
28
+ Object.defineProperty(exports, "getStateFilePath", { enumerable: true, get: function () { return stateFile_1.getStateFilePath; } });
29
+ Object.defineProperty(exports, "quotaToStateFile", { enumerable: true, get: function () { return stateFile_1.quotaToStateFile; } });
30
+ Object.defineProperty(exports, "readStateFile", { enumerable: true, get: function () { return stateFile_1.readStateFile; } });
31
+ Object.defineProperty(exports, "writeStateFile", { enumerable: true, get: function () { return stateFile_1.writeStateFile; } });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * One vocabulary for token totals across every Sidekick surface.
3
+ *
4
+ * Every report, dashboard, status bar, and export derives its headline token
5
+ * numbers from here so "Total" means the same thing everywhere:
6
+ *
7
+ * - `total` — every token the provider billed: uncached input, cache writes,
8
+ * cache reads, and output. Matches `NormalizedUsage.totalTokens`
9
+ * and the per-model `tokens` the `EventAggregator` keeps.
10
+ * - `context` — tokens that occupied the context window on the last request:
11
+ * uncached input, cache writes, and cache reads. Output is
12
+ * excluded, matching the input-only formula Claude Code uses for
13
+ * its status-line `used_percentage`.
14
+ * - `output` — output tokens as billed.
15
+ *
16
+ * Cache reads are usually the majority of a coding-agent session's tokens, so
17
+ * an "input + output" total silently drops most of the work. Sidekick used to
18
+ * do that on some surfaces and not others; this helper is the single place the
19
+ * arithmetic lives.
20
+ *
21
+ * Browser-safe: no Node imports.
22
+ *
23
+ * @module tokenSummary
24
+ */
25
+ /** Any object carrying the four billable token buckets. */
26
+ export interface TokenTotalsLike {
27
+ inputTokens: number;
28
+ outputTokens: number;
29
+ cacheWriteTokens?: number;
30
+ cacheReadTokens?: number;
31
+ /**
32
+ * A provider-semantics-aware total, when the source already computed one
33
+ * (for example `AggregatedTokens.totalTokens`). Preferred over the
34
+ * four-bucket sum because it already accounts for whether reasoning tokens
35
+ * were billed inside or outside `outputTokens`.
36
+ */
37
+ totalTokens?: number;
38
+ }
39
+ export interface TokenSummary {
40
+ /** Everything billed: uncached input + cache writes + cache reads + output. */
41
+ total: number;
42
+ /** Tokens occupying the context window: uncached input + cache writes + cache reads. */
43
+ context: number;
44
+ /** Uncached input tokens. */
45
+ input: number;
46
+ /** Output tokens as billed. */
47
+ output: number;
48
+ /** Tokens read from the prompt cache. */
49
+ cacheRead: number;
50
+ /** Tokens written to the prompt cache. */
51
+ cacheWrite: number;
52
+ /** Fraction of context tokens that came from cache reads (0..1), or null with no input. */
53
+ cacheHitRatio: number | null;
54
+ }
55
+ /** Column label for `TokenSummary.total` on any surface. */
56
+ export declare const TOKEN_TOTAL_LABEL = "Total (incl. cache)";
57
+ /** Column label for `TokenSummary.context` on any surface. */
58
+ export declare const TOKEN_CONTEXT_LABEL = "Context";
59
+ /** Derive the shared token vocabulary from any four-bucket token record. */
60
+ export declare function summarizeTokens(totals: TokenTotalsLike): TokenSummary;
61
+ /** Sum several token records bucket-by-bucket before summarizing. */
62
+ export declare function sumTokenTotals(records: readonly TokenTotalsLike[]): TokenTotalsLike;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * One vocabulary for token totals across every Sidekick surface.
4
+ *
5
+ * Every report, dashboard, status bar, and export derives its headline token
6
+ * numbers from here so "Total" means the same thing everywhere:
7
+ *
8
+ * - `total` — every token the provider billed: uncached input, cache writes,
9
+ * cache reads, and output. Matches `NormalizedUsage.totalTokens`
10
+ * and the per-model `tokens` the `EventAggregator` keeps.
11
+ * - `context` — tokens that occupied the context window on the last request:
12
+ * uncached input, cache writes, and cache reads. Output is
13
+ * excluded, matching the input-only formula Claude Code uses for
14
+ * its status-line `used_percentage`.
15
+ * - `output` — output tokens as billed.
16
+ *
17
+ * Cache reads are usually the majority of a coding-agent session's tokens, so
18
+ * an "input + output" total silently drops most of the work. Sidekick used to
19
+ * do that on some surfaces and not others; this helper is the single place the
20
+ * arithmetic lives.
21
+ *
22
+ * Browser-safe: no Node imports.
23
+ *
24
+ * @module tokenSummary
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.TOKEN_CONTEXT_LABEL = exports.TOKEN_TOTAL_LABEL = void 0;
28
+ exports.summarizeTokens = summarizeTokens;
29
+ exports.sumTokenTotals = sumTokenTotals;
30
+ /** Column label for `TokenSummary.total` on any surface. */
31
+ exports.TOKEN_TOTAL_LABEL = 'Total (incl. cache)';
32
+ /** Column label for `TokenSummary.context` on any surface. */
33
+ exports.TOKEN_CONTEXT_LABEL = 'Context';
34
+ function count(value) {
35
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
36
+ }
37
+ /** Derive the shared token vocabulary from any four-bucket token record. */
38
+ function summarizeTokens(totals) {
39
+ const input = count(totals.inputTokens);
40
+ const output = count(totals.outputTokens);
41
+ const cacheRead = count(totals.cacheReadTokens);
42
+ const cacheWrite = count(totals.cacheWriteTokens);
43
+ const context = input + cacheWrite + cacheRead;
44
+ const fourBucket = context + output;
45
+ const provided = totals.totalTokens;
46
+ const total = typeof provided === 'number' && Number.isFinite(provided) && provided >= 0
47
+ ? Math.floor(provided)
48
+ : fourBucket;
49
+ return {
50
+ total,
51
+ context,
52
+ input,
53
+ output,
54
+ cacheRead,
55
+ cacheWrite,
56
+ cacheHitRatio: context > 0 ? cacheRead / context : null,
57
+ };
58
+ }
59
+ /** Sum several token records bucket-by-bucket before summarizing. */
60
+ function sumTokenTotals(records) {
61
+ let inputTokens = 0;
62
+ let outputTokens = 0;
63
+ let cacheWriteTokens = 0;
64
+ let cacheReadTokens = 0;
65
+ for (const record of records) {
66
+ inputTokens += count(record.inputTokens);
67
+ outputTokens += count(record.outputTokens);
68
+ cacheWriteTokens += count(record.cacheWriteTokens);
69
+ cacheReadTokens += count(record.cacheReadTokens);
70
+ }
71
+ return { inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
72
+ }
@@ -77,7 +77,12 @@ export interface CanonicalSessionTranscript {
77
77
  events: NormalizedUsage[];
78
78
  costs: NormalizedUsageCost[];
79
79
  totals: CanonicalTranscriptUsageTotals;
80
+ /** Strict total: `null` unless every usage event could be priced. */
80
81
  totalCostUsd: number | null;
82
+ /** Sum of the events that could be priced; pair with `unpricedEvents`. */
83
+ pricedCostUsd: number;
84
+ /** Usage events with neither a reported cost nor a catalog price. */
85
+ unpricedEvents: number;
81
86
  };
82
87
  provenance: {
83
88
  source: 'session-events';
@@ -144,6 +144,8 @@ function projectSessionTranscript(events, options = {}) {
144
144
  totalCostUsd: usageCosts.length > 0 && usageCosts.every((cost) => cost.costUsd !== null)
145
145
  ? usageCosts.reduce((sum, cost) => sum + (cost.costUsd ?? 0), 0)
146
146
  : null,
147
+ pricedCostUsd: usageCosts.reduce((sum, cost) => sum + (cost.costUsd ?? 0), 0),
148
+ unpricedEvents: usageCosts.filter((cost) => cost.costUsd === null).length,
147
149
  },
148
150
  provenance: { source: 'session-events', fidelity, eventCount: events.length },
149
151
  };
@@ -69,6 +69,8 @@ export interface ProviderSessionAdapterV1 {
69
69
  dispose(): void;
70
70
  }
71
71
  export declare function derivePendingUserRequestV1(provider: ProviderId, sessionId: string, sourcePath: string, events: SessionEvent[]): PendingUserRequestV1 | null;
72
+ /** The activity detector's reason for a session produced by this adapter, if known. */
73
+ export declare function getObservedActivityReason(session: ObservedAgentSessionV1): string | undefined;
72
74
  export interface ProviderSessionAdapterV1Options {
73
75
  pollIntervalMs?: number;
74
76
  observationOnly?: boolean;
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.derivePendingUserRequestV1 = derivePendingUserRequestV1;
4
+ exports.getObservedActivityReason = getObservedActivityReason;
4
5
  exports.createProviderSessionAdapterV1 = createProviderSessionAdapterV1;
5
6
  const transcript_1 = require("../transcript");
6
7
  const sessionActivityDetector_1 = require("../parsers/sessionActivityDetector");
8
+ const sessionStats_1 = require("../sessionStats");
9
+ const sessionFingerprint_1 = require("../sessionFingerprint");
7
10
  function evidence(provider, sessionId, sourcePath, event, eventIndex) {
8
11
  return [
9
12
  {
@@ -111,6 +114,16 @@ function providerCapabilities(provider, observationOnly) {
111
114
  assetExtraction: known(true),
112
115
  };
113
116
  }
117
+ /**
118
+ * Detector reason behind each observed session's activity, kept off the
119
+ * versioned V1 shape so cache refreshes can tell a grace-period "active" from
120
+ * a genuinely ongoing one.
121
+ */
122
+ const activityReasons = new WeakMap();
123
+ /** The activity detector's reason for a session produced by this adapter, if known. */
124
+ function getObservedActivityReason(session) {
125
+ return activityReasons.get(session);
126
+ }
114
127
  function createProviderSessionAdapterV1(sessionProvider, options = {}) {
115
128
  const pollIntervalMs = options.pollIntervalMs ?? 2_000;
116
129
  const adapter = {
@@ -121,16 +134,28 @@ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
121
134
  return Promise.all(sessionProvider.findAllSessions(cwd).map((sessionPath) => adapter.read(sessionPath, cwd)));
122
135
  },
123
136
  async read(sessionPath, cwd = '') {
124
- const stats = sessionProvider.readSessionStats(sessionPath);
125
- const events = sessionProvider.createReader(sessionPath).readAll();
137
+ // One reader pass feeds stats, transcript, activity, and the pending
138
+ // request; the reader is flushed so a trailing partial line is included.
139
+ const sessionId = sessionProvider.getSessionId(sessionPath);
140
+ const reader = sessionProvider.createReader(sessionPath);
141
+ const events = reader.readAll();
142
+ reader.flush();
143
+ const stats = (0, sessionStats_1.computeSessionFileStats)(events, {
144
+ providerId: sessionProvider.id,
145
+ sessionId,
146
+ filePath: sessionPath,
147
+ label: '',
148
+ computeContextSize: (0, sessionStats_1.providerContextSizeFn)(sessionProvider),
149
+ });
126
150
  const transcript = (0, transcript_1.projectSessionTranscript)(events, {
127
151
  provider: sessionProvider.id,
128
- sessionId: stats.sessionId,
152
+ sessionId,
129
153
  sourcePath: sessionPath,
130
154
  });
131
- const refs = evidence(sessionProvider.id, stats.sessionId, sessionPath);
155
+ const refs = evidence(sessionProvider.id, sessionId, sessionPath);
132
156
  const model = Object.entries(stats.modelUsage).sort((left, right) => right[1].calls - left[1].calls)[0]?.[0] ?? null;
133
- const activityResult = (0, sessionActivityDetector_1.detectSessionActivity)(sessionPath);
157
+ const parts = (0, sessionFingerprint_1.sessionFingerprintParts)(sessionProvider, sessionPath);
158
+ const activityResult = (0, sessionActivityDetector_1.classifySessionActivity)({ events, mtimeMs: parts?.mtimeMs ?? null });
134
159
  const activity = activityResult.state === 'ongoing'
135
160
  ? 'active'
136
161
  : activityResult.state === 'stale'
@@ -139,11 +164,11 @@ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
139
164
  const allCostsReported = transcript.usage.costs.length > 0 &&
140
165
  transcript.usage.costs.every((cost) => cost.source === 'provider-reported');
141
166
  const observedAt = new Date().toISOString();
142
- return {
167
+ const session = {
143
168
  schemaVersion: 1,
144
169
  identity: {
145
170
  provider: sessionProvider.id,
146
- sessionId: stats.sessionId,
171
+ sessionId,
147
172
  sourcePath: sessionPath,
148
173
  },
149
174
  cwd: observed(cwd, 'inferred', cwd ? 0.95 : 0.4, refs),
@@ -161,29 +186,61 @@ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
161
186
  ? 'inferred'
162
187
  : 'estimated', allCostsReported ? 1 : transcript.usage.totalCostUsd === null ? 0.5 : 0.9, refs),
163
188
  },
164
- pendingUserRequest: derivePendingUserRequestV1(sessionProvider.id, stats.sessionId, sessionPath, events),
189
+ pendingUserRequest: derivePendingUserRequestV1(sessionProvider.id, sessionId, sessionPath, events),
165
190
  contentObservedAt: observedAt,
166
191
  observedAt,
167
192
  };
193
+ activityReasons.set(session, activityResult.reason);
194
+ return session;
168
195
  },
169
196
  watch(sessionPath, listener, cwd) {
170
197
  let disposed = false;
171
198
  let lastSignature = '';
199
+ let lastFingerprint = null;
200
+ let lastSession = null;
201
+ const emitIfChanged = (session) => {
202
+ const signature = JSON.stringify({
203
+ activity: session.activity,
204
+ model: session.model,
205
+ usage: session.usage,
206
+ pendingUserRequest: session.pendingUserRequest,
207
+ });
208
+ if (signature !== lastSignature) {
209
+ lastSignature = signature;
210
+ listener(session);
211
+ }
212
+ };
172
213
  const update = async () => {
173
214
  if (disposed)
174
215
  return;
175
216
  try {
176
- const session = await adapter.read(sessionPath, cwd);
177
- const signature = JSON.stringify({
178
- activity: session.activity,
179
- model: session.model,
180
- usage: session.usage,
181
- pendingUserRequest: session.pendingUserRequest,
182
- });
183
- if (signature !== lastSignature) {
184
- lastSignature = signature;
185
- listener(session);
217
+ const parts = (0, sessionFingerprint_1.sessionFingerprintParts)(sessionProvider, sessionPath);
218
+ const fingerprint = parts ? (0, sessionFingerprint_1.fingerprintString)(parts) : null;
219
+ if (lastSession && parts && fingerprint === lastFingerprint) {
220
+ // Unchanged content: only the activity can drift with time, so
221
+ // refresh it from the cached classification instead of re-reading.
222
+ const previousReason = activityReasons.get(lastSession);
223
+ const refreshed = (0, sessionActivityDetector_1.refreshSessionActivityState)(lastSession.activity.value, parts.mtimeMs, Date.now(), previousReason);
224
+ if (refreshed !== lastSession.activity.value) {
225
+ const session = {
226
+ ...lastSession,
227
+ activity: { ...lastSession.activity, value: refreshed },
228
+ observedAt: new Date().toISOString(),
229
+ };
230
+ activityReasons.set(session, refreshed === 'idle'
231
+ ? 'mtime-stale'
232
+ : previousReason === 'grace-period'
233
+ ? 'ending-event'
234
+ : 'no-activity-signal');
235
+ lastSession = session;
236
+ emitIfChanged(session);
237
+ }
238
+ return;
186
239
  }
240
+ const session = await adapter.read(sessionPath, cwd);
241
+ lastFingerprint = fingerprint;
242
+ lastSession = session;
243
+ emitIfChanged(session);
187
244
  }
188
245
  catch {
189
246
  // Observed sources can disappear between polling iterations.
@@ -47,6 +47,8 @@ export interface SessionMessage {
47
47
  normalizedUsage?: import('../usageNormalization').NormalizedUsage;
48
48
  /** Message content (may be string or structured content) */
49
49
  content?: unknown;
50
+ /** Provider stop reason (`end_turn`, `tool_use`, …) when the transcript carries one. */
51
+ stop_reason?: string;
50
52
  }
51
53
  /**
52
54
  * Canonical session event from JSONL/DB log files.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Five-hour billing blocks computed from usage events.
3
+ *
4
+ * Mirrors the ccusage convention: a block opens at the first usage event
5
+ * (aligned down to the UTC hour), lasts five hours, and an event that lands
6
+ * after the block's end or more than five hours after the previous event opens
7
+ * a new block. The active block also reports a burn rate and an end-of-block
8
+ * projection. Browser-safe: no Node imports.
9
+ *
10
+ * @module usage/billingBlocks
11
+ */
12
+ import type { AggregatedCostProvenance } from '../aggregation/types';
13
+ import type { TokenTotalsLike } from '../tokenSummary';
14
+ import type { PricingProvenance } from '../usageNormalization';
15
+ /** Length of one billing block. */
16
+ export declare const BILLING_BLOCK_DURATION_MS: number;
17
+ /** Minimal usage event the block calculator needs. */
18
+ export interface BillingBlockInput {
19
+ /** Event time (ms since epoch, ISO string, or Date). */
20
+ timestamp: number | string | Date;
21
+ tokens: TokenTotalsLike;
22
+ /** Priced cost for the event, or null when it could not be priced. */
23
+ costUsd: number | null;
24
+ costProvenance?: PricingProvenance;
25
+ model?: string;
26
+ }
27
+ export interface BillingBlockTokens {
28
+ inputTokens: number;
29
+ outputTokens: number;
30
+ cacheWriteTokens: number;
31
+ cacheReadTokens: number;
32
+ /** Cache-inclusive total (`summarizeTokens().total`). */
33
+ total: number;
34
+ }
35
+ export interface BillingBlockModelUsage {
36
+ calls: number;
37
+ /** Cache-inclusive tokens. */
38
+ tokens: number;
39
+ costUsd: number;
40
+ }
41
+ export interface BillingBlock {
42
+ /** Block identifier: the ISO start time. */
43
+ id: string;
44
+ /** ISO start (aligned to the hour when `alignToHour` is set). */
45
+ start: string;
46
+ /** ISO end (`start` + block duration). */
47
+ end: string;
48
+ firstEvent: string;
49
+ lastEvent: string;
50
+ /** `now` is inside the block and within one block length of the last event. */
51
+ isActive: boolean;
52
+ calls: number;
53
+ tokens: BillingBlockTokens;
54
+ costUsd: number;
55
+ costProvenance: AggregatedCostProvenance;
56
+ unpricedCalls: number;
57
+ models: Record<string, BillingBlockModelUsage>;
58
+ /** Time from the first event to `now` (active) or to the last event (closed). */
59
+ elapsedMs: number;
60
+ /** Time left until `end` for the active block; 0 otherwise. */
61
+ remainingMs: number;
62
+ /** Tokens per minute over `elapsedMs`. */
63
+ burnRatePerMinute: number;
64
+ /** Tokens expected by `end` at the current burn rate (equals `tokens.total` for closed blocks). */
65
+ projectedTokens: number;
66
+ /** Cost expected by `end` at the current cost per token (equals `costUsd` for closed blocks). */
67
+ projectedCostUsd: number;
68
+ }
69
+ export interface ComputeBillingBlocksOptions {
70
+ /** Clock used for activity and projections (default `Date.now()`). */
71
+ now?: Date | number;
72
+ /** Block length (default five hours). */
73
+ blockDurationMs?: number;
74
+ /** Align each block's start down to the UTC hour (default true, as ccusage does). */
75
+ alignToHour?: boolean;
76
+ }
77
+ /**
78
+ * Group usage events into billing blocks, oldest first.
79
+ *
80
+ * Events with an unparseable timestamp are ignored. Events are sorted before
81
+ * grouping, so callers may pass them in any order.
82
+ */
83
+ export declare function computeBillingBlocks(events: readonly BillingBlockInput[], options?: ComputeBillingBlocksOptions): BillingBlock[];
84
+ /** The block that is still open at the calculator's `now`, if any. */
85
+ export declare function findActiveBillingBlock(blocks: readonly BillingBlock[]): BillingBlock | null;