sidekick-shared 0.27.2 → 0.27.4

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 (51) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +57 -14
  3. package/dist/aggregation/EventAggregator.d.ts +12 -2
  4. package/dist/aggregation/EventAggregator.js +49 -19
  5. package/dist/browser.d.ts +2 -1
  6. package/dist/browser.js +9 -2
  7. package/dist/extractors/tokenUsage.js +1 -0
  8. package/dist/formatters/sessionDump.d.ts +10 -2
  9. package/dist/formatters/sessionDump.js +45 -23
  10. package/dist/index.d.ts +7 -3
  11. package/dist/index.js +31 -10
  12. package/dist/node.d.ts +6 -1
  13. package/dist/node.js +12 -2
  14. package/dist/parsers/codexParser.d.ts +22 -2
  15. package/dist/parsers/codexParser.js +64 -5
  16. package/dist/parsers/subagentScanner.d.ts +9 -0
  17. package/dist/parsers/subagentScanner.js +45 -12
  18. package/dist/parsers/subagentStatsCache.d.ts +17 -0
  19. package/dist/parsers/subagentStatsCache.js +82 -0
  20. package/dist/parsers/subagentTraceParser.js +19 -10
  21. package/dist/providers/claudeCode.js +8 -41
  22. package/dist/providers/codex.js +28 -14
  23. package/dist/providers/codexDatabase.d.ts +16 -1
  24. package/dist/providers/codexDatabase.js +49 -1
  25. package/dist/providers/openCode.js +30 -4
  26. package/dist/report/htmlReportGenerator.js +33 -11
  27. package/dist/report/sessionReportInputs.d.ts +3 -1
  28. package/dist/report/sessionReportInputs.js +2 -0
  29. package/dist/report/types.d.ts +5 -0
  30. package/dist/schemas/sessionEvent.d.ts +30 -6
  31. package/dist/schemas/sessionEvent.js +7 -1
  32. package/dist/schemas/stateFile.d.ts +2 -0
  33. package/dist/schemas/stateFile.js +1 -0
  34. package/dist/sessionTokenTotals.d.ts +46 -0
  35. package/dist/sessionTokenTotals.js +138 -0
  36. package/dist/stateFile.d.ts +7 -0
  37. package/dist/tokenSummary.d.ts +27 -1
  38. package/dist/tokenSummary.js +50 -3
  39. package/dist/types/codex.d.ts +4 -0
  40. package/dist/types/sessionEvent.d.ts +22 -0
  41. package/dist/usage/claudeUsageDedupe.d.ts +53 -0
  42. package/dist/usage/claudeUsageDedupe.js +195 -0
  43. package/dist/usage/subagentUsage.d.ts +13 -0
  44. package/dist/usage/subagentUsage.js +21 -0
  45. package/dist/usage/usageEvents.d.ts +18 -2
  46. package/dist/usage/usageEvents.js +83 -8
  47. package/dist/watchers/eventBridge.js +5 -0
  48. package/dist/watchers/jsonlWatcher.d.ts +5 -0
  49. package/dist/watchers/jsonlWatcher.js +40 -5
  50. package/dist/watchers/types.d.ts +5 -0
  51. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to sidekick-shared will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.27.4] - 2026-09-23
9
+
10
+ ### Added
11
+
12
+ - `sidekick-shared/node` exports `createSessionProviders()`, the `CreateSessionProvidersOptions` / `CreateSessionProvidersResult` types, `SYNTHETIC_MODEL`, and the `ProviderId` / `SessionProviderBase` / `SessionProviderDiagnostic` types, re-exported from their own modules. `collectUsageEvents()` callers no longer need the package root, a CommonJS barrel that bundlers cannot tree-shake (about 420 KB minified saved in one esbuild consumer). Root exports are unchanged.
13
+ - `CollectUsageEventsOptions.signal` (optional `AbortSignal`): `collectUsageEvents()` checks it before listing sessions and between sessions, and rejects with the signal's reason (`AbortError` by default). Only fully read sessions are cached. Without a signal nothing changes; counting, pricing, and the cache format (`USAGE_CACHE_VERSION` 2) are the same.
14
+ - A packaging test walks every relative `require` reachable from `dist/node.js` and fails if it reaches `dist/index.js`.
15
+
16
+ ## [0.27.3] - 2026-09-23
17
+
18
+ ### Fixed
19
+
20
+ - Claude Code split lines are counted once. `ClaudeCodeReader` runs every event through the new `ClaudeUsageDeduper`: the first line of a `message.id` carries its usage, identical repeats lose `usage`/`normalizedUsage`, and a repeat that grew (streamed `output_tokens`) carries only the growth with `message.usageKind: 'correction'`. `EventAggregator` adds corrections to tokens, cost, and per-model tokens without counting a call, context sample, or compaction; the usage cache merges them into their message's record. The CLI follow watcher (`JsonlSessionWatcher`) and `eventBridge` apply the same rule and carry `FollowEvent.usageKind`.
21
+ - Codex duplicate `token_count` events are counted once. New `resolveCodexCallUsage(info, previousTotal)`: an unchanged `total_token_usage` yields no usage (rate limits still pass through), a missing `last_token_usage` yields the growth of the cumulative total instead of the total, and a total that drops re-baselines. Used by `CodexRolloutParser` and the Codex branch of `JsonlSessionWatcher`, which also splits cached input into `cacheTokens` now.
22
+ - `sessionMessageSchema` accepts `stop_reason: null` (normalized to absent); such lines used to fail validation and drop the whole event.
23
+ - `<synthetic>` Claude messages carry no usage, so they add no call, unpriced model row, or heuristic compaction.
24
+ - Codex `scanSubagents()` finds spawned subagents through the new `CodexDatabase.getSpawnedChildThreads()` (`threads.source`), and `getThreadsByForkedFromId()` only queries when the column exists. The filesystem fallback also matches `source.subagent.thread_spawn.parent_thread_id`.
25
+ - `SubagentStats` from every provider (Claude scanners, Codex, OpenCode) now fill `cacheReadTokens`, `cacheWriteTokens`, `reasoningTokens`, and `totalTokens` from normalized, deduplicated usage. Claude scanners read `agent-*.meta.json` (`readAgentMeta()`). Parsed subagent transcripts are cached by size and mtime.
26
+ - Timeline `metadata.tokenCount` and the HTML report's per-message tokens are cache-inclusive totals instead of input + output.
27
+
28
+ ### Added
29
+
30
+ - `sessionTokenTotals`: `collectSessionTokenTotals(provider, sessionPath, { mainThread? })`, `combineSessionTokenTotals()`, `readMainThreadTokenTotals()`, `scanSessionSubagents()`, `subagentTokenTotals()`, and the `SessionTokenTotals` / `SubagentTokenSummary` types: main thread, per-subagent, subagent total, and combined, in the `summarizeTokens` vocabulary.
31
+ - `formatTokenBreakdown(summary, fmt?)`; `TokenSummary.reasoning` and `billedOutsideBuckets`; `TokenTotalsLike.reasoningTokens`; labels `TOKEN_MAIN_THREAD_LABEL`, `TOKEN_SUBAGENTS_LABEL`, `TOKEN_SESSION_TOTAL_LABEL`. `sumTokenTotals()` sums `reasoningTokens`, and `totalTokens` when every record has one.
32
+ - `formatSessionText`/`formatSessionMarkdown`/`formatSessionJson` and `HtmlReportOptions` accept `subagents`; the JSON dump adds `tokenSummary` (`mainThread`, `subagents`, `subagentTotal`, `combined`). `readSessionReportInputs()` returns `subagents`.
33
+ - `usageKind` on `SessionMessage`, `TokenUsage` (from `extractTokenUsage`), and `FollowEvent`; `StateFileContext.totalTokens` (optional); `CodexDbThread.source`, `agent_nickname`, `agent_role`.
34
+ - Exports: `ClaudeUsageDeduper`, `SYNTHETIC_MODEL`, `dedupedRawClaudeUsage`, `normalizeClaudeUsage`, `addUsageToSubagent`, `resolveCodexCallUsage`, `readAgentMeta` (root); the session-total helpers also from `sidekick-shared/node`; `ClaudeUsageDeduper` and `SYNTHETIC_MODEL` from `sidekick-shared/browser`.
35
+
36
+ ### Changed
37
+
38
+ - `collectUsageEvents()` reads Claude Code subagent transcripts (`<sessionId>/subagents/agent-*.jsonl`) with their parent session, and their sizes and mtimes join the fingerprint. Codex subagents are separate rollouts and are not added twice.
39
+ - `USAGE_CACHE_VERSION` is 2 and `SNAPSHOT_SCHEMA_VERSION` is 6, so caches built with the old counting are re-read.
40
+ - The text dump's compaction line reads "Context management:" and per-model token columns use "Total (incl. cache)"; the HTML report's "Cache" card is now "Cache hit".
41
+
8
42
  ## [0.27.2] - 2026-09-22
9
43
 
10
44
  ### Fixed
package/README.md CHANGED
@@ -9,6 +9,8 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
+ - **0.27.4: usage events from the Node entry** — `sidekick-shared/node` now exports `createSessionProviders()`, `SYNTHETIC_MODEL`, and the `ProviderId` / `SessionProviderBase` / `SessionProviderDiagnostic` / `CreateSessionProviders*` types, so `collectUsageEvents()` callers never import the package root (a CommonJS barrel that bundlers cannot tree-shake). `collectUsageEvents()` accepts an optional `signal` that stops the collection before listing and between sessions.
13
+ - **0.27.3: honest session totals** — Claude Code split lines are counted once: `ClaudeUsageDeduper` keeps a `message.id`'s first usage, strips identical repeats, and turns streamed growth into a `usageKind: 'correction'` delta that `EventAggregator` adds to tokens and cost without counting another call. `resolveCodexCallUsage()` drops duplicate Codex `token_count` events. `collectSessionTokenTotals()` (root and Node) returns `mainThread`, per-`subagents`, `subagentTotal`, and `combined` in the `summarizeTokens()` vocabulary, and `formatTokenBreakdown()` renders one line whose parts add up to the headline. `TokenSummary` gains `reasoning` / `billedOutsideBuckets`, and `SubagentStats` from every provider now carries cache, reasoning, and total tokens. The session dumps and `HtmlReportOptions` accept `subagents`, `readSessionReportInputs()` returns them, `collectUsageEvents()` reads Claude Code subagent transcripts, and `USAGE_CACHE_VERSION` 2 re-reads caches built with the old counting.
12
14
  - **0.27.2: streamed prompt history** — `collectPromptHistory()` reads session logs of any size in bounded, resumable passes instead of skipping files over `maxFileBytes`: every call keeps what it read, the cursor (`version: 3`) continues where each file stopped, and a pending prompt keeps its answer state so a far-away answer still settles it. Adds `maxRecordBytes` and `maxEntries` bounds, `hasMore`, permanently skipped records in `exclusions`, and rewrite detection.
13
15
  - **0.27.1: late prompt metadata** — `collectPromptHistory()` returns a prompt again, with the same provider, sessionId, ordinal, timestamp, and text, when its first answering call is written after an earlier collection, and entries gain `metadataStatus` (`pending` / `provisional` / `final`). The cursor becomes `version: 2`, keeps a replay offset and metadata digest for the one open prompt per file (no prompt text), and still accepts 0.27.0 cursors.
14
16
  - **0.27.0: human prompt history** — `collectPromptHistory()` (Node-only) returns the prompts a person typed in Claude Code and Codex sessions under a set of workspace roots (plus git worktree siblings), with fail-closed cwd scoping, untruncated text, stable per-session ordinals, the answering model and usage, a JSON-serializable resume cursor, and bounds that stop cleanly and report themselves in `boundsHit`. `isHumanPrompt()` / `humanPromptText()` expose the same classification for any canonical transcript message, and transcript provenance now keeps Claude Code's `isCompactSummary`, `originKind`, and `promptSource`. Claude Opus 5.5 gets its own pricing and context entries.
@@ -100,14 +102,14 @@ returns the platform default that both overrides bypass.
100
102
  | **Observed Sessions (V1)** | Versioned provider-neutral observation contracts — `createProviderSessionAdapterV1()` / `derivePendingUserRequestV1()` with `ObservedAgentSessionV1`, `ProviderCapabilitiesV1`, `PendingUserRequestV1`, `SessionEvidenceRefV1`, and provenance/confidence wrapper types (Zod schemas in `sidekick-shared/schemas`) |
101
103
  | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
102
104
  | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
103
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
105
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), the shared token vocabulary (`summarizeTokens()`, `formatTokenBreakdown()`, `TOKEN_*_LABEL`s) and session totals incl. subagents (`collectSessionTokenTotals()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
104
106
  | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
105
107
  | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
106
108
  | **Analytics** | Session-quality scoring (beta) with factor contributions and weekly trends (`scoreSessionQuality()`, `calculateQualityTrend()`), code-impact metrics including cost per changed line and per-model churn/cost (`calculateCodeImpact()`), and reported-vs-heuristic compaction accounting (`calculateCompactionLedger()`, `formatCompactionLedger()`) |
107
109
  | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
108
110
  | **Session Context** | Provider-neutral context evidence snapshots (`buildSessionContextSnapshot()`, `calculateSessionContextPressure()`, `createSessionContextProjector()`, `readSessionContextSnapshot()`): layered evidence sources, low/medium/high context pressure, and observed capabilities (tools, MCP servers, permission mode, rate limits) |
109
111
  | **Assistant Turns** | Browser-safe timeline/process/answer projection for provider-normalized assistant turns (`segmentAssistantTurn()`, `assistantTurnEventsFromSessionEvents()`), including interleaved reasoning, compact tool groups, and Claude `Task` subagent refs without prompt leakage |
110
- | **Report** | Self-contained HTML session report generation |
112
+ | **Report** | Self-contained HTML session report generation; `readSessionReportInputs()` returns metrics, transcript, and `subagents` for the session total |
111
113
  | **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows) |
112
114
  | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams (plus Codex reset credits via `fetchCodexResetCreditsFromApi()`, attached to quota state by `fetchCodexQuotaFromApi()`), and authoritative z.ai Coding Plan quota fetching via `resolveZaiQuota()` / `fetchZaiQuotaFromApi()` (reads z.ai's `api/monitor/usage/quota/limit` endpoint with cached-snapshot fallback). The observed-traffic z.ai estimator was removed from the package root exports in 0.23.0 and every remaining export of it is `@deprecated` — `resolveZaiQuota()` is the single supported z.ai quota API |
113
115
  | **Provider Status** | `fetchProviderServiceStatus()` returns explicit observed/unavailable public status with components and unresolved incidents from status.claude.com and status.openai.com; `fetchProviderStatus()` / `fetchOpenAIStatus()` remain as lossy legacy projections (`toLegacyProviderStatus()`) |
@@ -117,7 +119,7 @@ returns the platform default that both overrides bypass.
117
119
  | **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5.1 / Opus 5.5 / Opus 5 / Opus 4.8 / Opus 4.7 1M and GPT-5.x / GPT-6 variants), pricing tables with optional LiteLLM hydration, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids |
118
120
  | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback. Stays dormant while no account exists and wakes through the account-change signal |
119
121
  | **Multi-Provider Quota** | `MultiProviderQuotaService` orchestrates Claude polling + peak-hours + account labels + Codex/z.ai quota updates behind one typed event stream. `CodexQuotaWatcher` watches the active Codex rollout for live rate limits with snapshot fallback; z.ai quota uses the API resolver with snapshot fallback. All three stay dormant without a matching account and wake on account changes |
120
- | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for startup bootstrap (migrations, abandoned-login cleanup, then `syncLiveAccountState()`), `syncLiveAccountState()` to register live logins the registry has never seen under their email, fold rotated tokens into their profile, and merge duplicate Codex profiles, `getAccountHealth()` / `listAccountsWithHealth()` for secret-free `fresh`/`expiring`/`expired`/`unknown`/`missing` health with access/refresh expiry,`getActiveAccountStatus()` for a single-pass active-account read across providers,`onAccountsChanged()`for push-based login/logout/switch notification (process-local mutations, filesystem watches, and a catch-up poll), and live-first`resolveActiveClaudeAccount()`/`resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
122
+ | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for startup bootstrap (migrations, abandoned-login cleanup, then `syncLiveAccountState()`), `syncLiveAccountState()` to register live logins the registry has never seen under their email, fold rotated tokens into their profile, and merge duplicate Codex profiles, `getAccountHealth()` / `listAccountsWithHealth()` for secret-free `fresh`/`expiring`/`expired`/`unknown`/`missing` health with access/refresh expiry, `getActiveAccountStatus()` for a single-pass active-account read across providers, `onAccountsChanged()` for push-based login/logout/switch notification (process-local mutations, filesystem watches, and a catch-up poll), and live-first `resolveActiveClaudeAccount()` /`resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
121
123
  | **Account Management 2.0** | Provider-neutral acquisition + switching: `beginAccountLogin()` / `getAccountLoginStatus()` / `finalizeAccountLogin()` / `spawnAccountLogin()` drive a TTY-less, profile-isolated login that doesn't disturb the active account until finalization; `listAllAccounts()` and `switchAccount()` expose a shared switcher across Claude and Codex; as of 0.26.6 switches are two-phase and verified, return a `SwitchAccountResult` with `runningConsumers`, `needsLogin`, and an `undoToken` (`undoLastSwitch()`), refuse expired or missing credentials unless forced, and `beginAccountLogin({ existingAccountId })` re-authenticates a saved profile in place. Every status/finalize/switch entry point also has an `-Async` variant (`getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `switchAccountAsync()`, …) that keeps `codex` CLI probes off the event loop — prefer those from hosts with a UI loop. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
122
124
  | **Isolated launch & auto-switch** | `getAccountLaunchEnv()` builds an isolated `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment (plus `envUnset`) for running a CLI as a saved account without switching, `writeLauncher()` / `removeLauncher()` write POSIX launcher scripts, `detectRunningAccountConsumers()` / `findRunningProcesses()` find CLIs and desktop apps that hold a login (`ps` / `tasklist`), `refreshInactiveAccounts()` is an opt-in keep-alive through the official CLIs, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) moves to a healthier saved account when quota crosses a configured threshold; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
123
125
  | **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — switching atomically swaps the profile's backed-up credentials into the system `~/.codex/auth.json`, with rotated-token staleness protection, one-time dual-home migration, and legacy multi-home session monitoring |
@@ -129,17 +131,17 @@ returns the platform default that both overrides bypass.
129
131
 
130
132
  `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime. As of 0.25.0 every documented subpath declares explicit `types` / `import` / `require` / `default` export conditions and resolves under Vite and TypeScript's legacy `moduleResolution: node` without consumer-side aliases.
131
133
 
132
- | Path | Runtime | What it exposes |
133
- | ------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
135
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
136
- | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, public provider service status (`fetchProviderServiceStatus()`), observed context-window persistence, recent-session/transcript reads, human prompt history (`collectPromptHistory()`), session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
137
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
138
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
139
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
140
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
141
- | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, account management, quota history, the `state.json` contract) — fs-free, no Node builtins. |
142
- | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers and writers, `formatStatusline()`, burn-rate estimates, quota freshness helpers, `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()`, and `readStateFile()` / `writeStateFile()`. |
134
+ | Path | Runtime | What it exposes |
135
+ | ------------------------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
137
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
138
+ | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, public provider service status (`fetchProviderServiceStatus()`), observed context-window persistence, recent-session/transcript reads, human prompt history (`collectPromptHistory()`), usage events from session logs (`collectUsageEvents()` with `createSessionProviders()` and `SYNTHETIC_MODEL`), session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
139
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
140
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
141
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
142
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
143
+ | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, account management, quota history, the `state.json` contract) — fs-free, no Node builtins. |
144
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers and writers, `formatStatusline()`, burn-rate estimates, quota freshness helpers, `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()`, and `readStateFile()` / `writeStateFile()`. |
143
145
 
144
146
  ### Browser / webview runtimes
145
147
 
@@ -272,6 +274,26 @@ for (const provider of providers) {
272
274
 
273
275
  Every provider computes `SessionFileStats` the same way — one reader pass through the shared `EventAggregator` — so per-model `tokens` are cache-inclusive, `costUsd` carries `costProvenance`, `toolFailures` splits out failed calls, and compaction and truncation counts are real for Claude Code, Codex, and OpenCode alike. `computeSessionFileStats(events, options)` does the same for events you already hold, and `firstUserPrompt(events)` derives the label.
274
276
 
277
+ `readSessionStats()` covers the session's own transcript. Subagents run in their own transcripts, so for a session total that includes them use `collectSessionTokenTotals()` (pass `{ mainThread }` when you already aggregated the transcript to skip re-reading it):
278
+
279
+ ```typescript
280
+ import {
281
+ ClaudeCodeProvider,
282
+ collectSessionTokenTotals,
283
+ formatTokenBreakdown,
284
+ formatTokenCount,
285
+ TOKEN_SESSION_TOTAL_LABEL,
286
+ } from 'sidekick-shared';
287
+
288
+ const provider = new ClaudeCodeProvider();
289
+ const totals = collectSessionTokenTotals(provider, '/path/to/session.jsonl');
290
+ // { mainThread, subagents: [{ agentId, agentType?, description?, summary }], subagentTotal, combined }
291
+ console.log(TOKEN_SESSION_TOTAL_LABEL, formatTokenBreakdown(totals.combined, formatTokenCount));
292
+ for (const agent of totals.subagents) console.log(agent.agentId, agent.summary.total);
293
+ ```
294
+
295
+ Every `TokenSummary` reports `total`, `context`, the four buckets, `reasoning`, and `billedOutsideBuckets` (reasoning billed outside `output`), and `formatTokenBreakdown()` shows that part only when it is non-zero, so the parts always add up to `total`.
296
+
275
297
  ### Read canonical session history
276
298
 
277
299
  ```typescript
@@ -460,6 +482,27 @@ const { providers, diagnostics } = createSessionProviders({
460
482
  Repeated identical degradations are coalesced, so the diagnostic stream stays
461
483
  bounded across long-lived polling and subscription cycles.
462
484
 
485
+ ### Collect usage events from the Node entry
486
+
487
+ Everything `collectUsageEvents()` needs is exported from `sidekick-shared/node`,
488
+ which never loads the package root, so bundlers keep only what you use:
489
+
490
+ ```typescript
491
+ import { collectUsageEvents, createSessionProviders, SYNTHETIC_MODEL } from 'sidekick-shared/node';
492
+
493
+ const { providers } = createSessionProviders({ providerIds: ['claude-code', 'codex'] });
494
+ const { events } = await collectUsageEvents({
495
+ providers,
496
+ since: '2026-09-01',
497
+ workspacePath: '/path/to/repo',
498
+ signal: AbortSignal.timeout(30_000), // optional: stop before listing and between sessions
499
+ });
500
+ const priced = events.filter((event) => event.model !== SYNTHETIC_MODEL);
501
+ ```
502
+
503
+ On abort the promise rejects with the signal's reason (an `AbortError` by
504
+ default); sessions read before the abort keep their cache entries.
505
+
463
506
  ### Detect the active session provider
464
507
 
465
508
  For a single-provider surface, auto-detect and construct just one:
@@ -13,8 +13,8 @@ import type { EventAggregatorOptions, AggregatedTokens, ModelUsageStats, BurnRat
13
13
  import type { SerializedFrequencyState } from './FrequencyTracker';
14
14
  import type { SerializedHeatmapState } from './HeatmapTracker';
15
15
  import type { SerializedPatternState } from './PatternExtractor';
16
- /** Schema version for serialized snapshots. */
17
- export declare const SNAPSHOT_SCHEMA_VERSION = 5;
16
+ /** Schema version for serialized snapshots. v6: token totals count each Claude response and Codex call once. */
17
+ export declare const SNAPSHOT_SCHEMA_VERSION = 6;
18
18
  /**
19
19
  * JSON-serializable snapshot of EventAggregator state.
20
20
  * Used by session snapshot sidecar files for fast resume.
@@ -187,7 +187,17 @@ export declare class EventAggregator {
187
187
  serialize(): SerializedAggregatorState;
188
188
  /** Restores mutable state from a serialized snapshot. Clears transient state (pending calls). */
189
189
  restore(state: SerializedAggregatorState): boolean;
190
+ /**
191
+ * Add one usage record to the running totals. A `'correction'` tops up a call
192
+ * already counted (a later split line of the same response grew), so it adds
193
+ * tokens and cost but no call, context sample, or compaction check.
194
+ */
190
195
  private accumulateUsage;
196
+ /**
197
+ * Per-model usage. Accumulate reasoning tokens and an inverted-sticky
198
+ * `priced` flag so one unpriced event taints the aggregate (UI renders "—").
199
+ */
200
+ private accumulateModelUsage;
191
201
  private updateBurnRate;
192
202
  private processLatency;
193
203
  private extractToolsFromContent;
@@ -30,8 +30,8 @@ const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
30
30
  const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
31
31
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
32
32
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
33
- /** Schema version for serialized snapshots. */
34
- exports.SNAPSHOT_SCHEMA_VERSION = 5;
33
+ /** Schema version for serialized snapshots. v6: token totals count each Claude response and Codex call once. */
34
+ exports.SNAPSHOT_SCHEMA_VERSION = 6;
35
35
  // ── Goal gate detection regex ──
36
36
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
37
37
  // ── Todo dependency parsing ──
@@ -206,7 +206,7 @@ class EventAggregator {
206
206
  // 5. Token accumulation
207
207
  const normalizedUsage = (0, usageNormalization_1.extractNormalizedUsage)(event);
208
208
  if (normalizedUsage) {
209
- const usageCost = this.accumulateUsage(normalizedUsage, event.timestamp, event.message.model);
209
+ const usageCost = this.accumulateUsage(normalizedUsage, event.timestamp, event.message.model, event.message.usageKind);
210
210
  this.attributePlanUsage(normalizedUsage.totalTokens, usageCost);
211
211
  }
212
212
  if (event.type !== 'system') {
@@ -265,7 +265,7 @@ class EventAggregator {
265
265
  reasoningIncludedInOutput: false,
266
266
  reportedCostUsd: event.cost,
267
267
  });
268
- const cost = this.accumulateUsage(normalized, event.timestamp, event.model);
268
+ const cost = this.accumulateUsage(normalized, event.timestamp, event.model, event.usageKind);
269
269
  this.attributePlanUsage(normalized.totalTokens, cost);
270
270
  }
271
271
  else if (typeof event.cost === 'number' && Number.isFinite(event.cost) && event.cost >= 0) {
@@ -637,7 +637,13 @@ class EventAggregator {
637
637
  // ═══════════════════════════════════════════════════════════════════════
638
638
  // Private: Token & Context
639
639
  // ═══════════════════════════════════════════════════════════════════════
640
- accumulateUsage(usage, timestamp, model) {
640
+ /**
641
+ * Add one usage record to the running totals. A `'correction'` tops up a call
642
+ * already counted (a later split line of the same response grew), so it adds
643
+ * tokens and cost but no call, context sample, or compaction check.
644
+ */
645
+ accumulateUsage(usage, timestamp, model, kind = 'call') {
646
+ const isCall = kind !== 'correction';
641
647
  const inputTok = usage.uncachedInputTokens;
642
648
  const outputTok = usage.outputTokens;
643
649
  const cacheWrite = usage.cacheWriteTokens;
@@ -654,19 +660,25 @@ class EventAggregator {
654
660
  this.reasoningTokens += reasoningTok;
655
661
  this.totalTokens += usage.totalTokens;
656
662
  this.costUsd += cost;
663
+ const callIncrement = isCall ? 1 : 0;
657
664
  if (pricedUsage.source === 'provider-reported') {
658
665
  this.reportedCostUsd += cost;
659
- this.reportedCalls += 1;
666
+ this.reportedCalls += callIncrement;
660
667
  }
661
668
  else if (pricedUsage.source === 'unpriced') {
662
- this.unpricedCalls += 1;
669
+ this.unpricedCalls += callIncrement;
663
670
  }
664
671
  else {
665
672
  this.estimatedCostUsd += cost;
666
- this.estimatedCalls += 1;
673
+ this.estimatedCalls += callIncrement;
667
674
  }
668
675
  // Burn rate sample accumulation
669
676
  this.tokensSinceLastSample += usage.totalTokens;
677
+ if (!isCall) {
678
+ this.accumulateModelUsage(usage, model, cost, priced, false);
679
+ this.updateBurnRate(timestamp);
680
+ return cost;
681
+ }
670
682
  // Context size computation
671
683
  let contextSize;
672
684
  if (this.computeContextSize) {
@@ -706,8 +718,21 @@ class EventAggregator {
706
718
  if (this.contextTimeline.length > this.contextTimelineCap) {
707
719
  this.contextTimeline.splice(0, this.contextTimeline.length - this.contextTimelineCap);
708
720
  }
709
- // Per-model usage. Accumulate reasoning tokens and an inverted-sticky
710
- // `priced` flag so one unpriced event taints the aggregate (UI renders "—").
721
+ this.accumulateModelUsage(usage, model, cost, priced, true);
722
+ // Burn rate sampling
723
+ this.updateBurnRate(timestamp);
724
+ return cost;
725
+ }
726
+ /**
727
+ * Per-model usage. Accumulate reasoning tokens and an inverted-sticky
728
+ * `priced` flag so one unpriced event taints the aggregate (UI renders "—").
729
+ */
730
+ accumulateModelUsage(usage, model, cost, priced, countCall) {
731
+ const inputTok = usage.uncachedInputTokens;
732
+ const outputTok = usage.outputTokens;
733
+ const cacheWrite = usage.cacheWriteTokens;
734
+ const cacheRead = usage.cacheReadTokens;
735
+ const reasoningTok = usage.reasoningTokens;
711
736
  const perModelKey = model ?? this.currentModel ?? 'unknown';
712
737
  const acc = this.modelUsage.get(perModelKey) ?? {
713
738
  calls: 0,
@@ -720,7 +745,8 @@ class EventAggregator {
720
745
  cost: 0,
721
746
  priced: true,
722
747
  };
723
- acc.calls++;
748
+ if (countCall)
749
+ acc.calls++;
724
750
  acc.tokens += usage.totalTokens;
725
751
  acc.inputTokens += inputTok;
726
752
  acc.outputTokens += outputTok;
@@ -731,9 +757,6 @@ class EventAggregator {
731
757
  if (!priced)
732
758
  acc.priced = false;
733
759
  this.modelUsage.set(perModelKey, acc);
734
- // Burn rate sampling
735
- this.updateBurnRate(timestamp);
736
- return cost;
737
760
  }
738
761
  // ═══════════════════════════════════════════════════════════════════════
739
762
  // Private: Burn Rate
@@ -1813,9 +1836,11 @@ class EventAggregator {
1813
1836
  noiseLevel = 'ai';
1814
1837
  if (event.message.model)
1815
1838
  metadata.model = event.message.model;
1816
- if (event.message.usage) {
1817
- metadata.tokenCount =
1818
- event.message.usage.input_tokens + event.message.usage.output_tokens;
1839
+ {
1840
+ // Cache-inclusive, like every other token total (see tokenSummary).
1841
+ const usage = (0, usageNormalization_1.extractNormalizedUsage)(event);
1842
+ if (usage)
1843
+ metadata.tokenCount = usage.totalTokens;
1819
1844
  }
1820
1845
  break;
1821
1846
  case 'tool_use': {
@@ -1892,8 +1917,13 @@ class EventAggregator {
1892
1917
  noiseLevel = 'ai';
1893
1918
  if (event.model)
1894
1919
  metadata.model = event.model;
1895
- if (event.tokens)
1896
- metadata.tokenCount = event.tokens.input + event.tokens.output;
1920
+ if (event.tokens) {
1921
+ metadata.tokenCount =
1922
+ event.tokens.input +
1923
+ event.tokens.output +
1924
+ (event.cacheTokens?.read ?? 0) +
1925
+ (event.cacheTokens?.write ?? 0);
1926
+ }
1897
1927
  break;
1898
1928
  case 'tool_use':
1899
1929
  tlType = 'tool_call';
package/dist/browser.d.ts CHANGED
@@ -11,8 +11,9 @@ export { getModelContextWindowSize, resolveModelContextWindow, DEFAULT_CONTEXT_W
11
11
  export type { ModelCatalogMatch, ModelContextWindowProvenance, ModelContextWindowSource, ResolvedModelContextWindow, } from './modelContext';
12
12
  export { normalizeProviderUsage, extractNormalizedUsage, calculateNormalizedUsageCost, } from './usageNormalization';
13
13
  export type { UsageProvider, UsageSemantics, UsageNormalizationProvenance, ProviderUsageInput, AnthropicUsageInput, OpenAIUsageInput, SidekickUsageInput, NormalizedUsage, PricingProvenance, NormalizedUsageCostInput, NormalizedUsageCost, } from './usageNormalization';
14
- export { summarizeTokens, sumTokenTotals, TOKEN_CONTEXT_LABEL, TOKEN_TOTAL_LABEL, } from './tokenSummary';
14
+ export { formatTokenBreakdown, summarizeTokens, sumTokenTotals, TOKEN_CONTEXT_LABEL, TOKEN_MAIN_THREAD_LABEL, TOKEN_SESSION_TOTAL_LABEL, TOKEN_SUBAGENTS_LABEL, TOKEN_TOTAL_LABEL, } from './tokenSummary';
15
15
  export type { TokenSummary, TokenTotalsLike } from './tokenSummary';
16
+ export { ClaudeUsageDeduper, SYNTHETIC_MODEL } from './usage/claudeUsageDedupe';
16
17
  export { estimateTextTokens, estimateSerializedTokens } from './tokenEstimation';
17
18
  export type { ExactTokenCounterContext, ExactTokenCounter, TokenEstimationMethod, TokenEstimationConfidence, TokenEstimationOptions, TokenEstimate, } from './tokenEstimation';
18
19
  export { projectSessionTranscript } from './transcript';
package/dist/browser.js CHANGED
@@ -9,8 +9,8 @@
9
9
  * For pricing hydration (LiteLLM catalog refresh), use `sidekick-shared/node`.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = exports.resolveModelAlias = exports.registerModelAlias = exports.importResolvedModelCatalog = exports.exportResolvedModelCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.resolveModelPricing = exports.getModelPricing = exports.parseModelId = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.SESSION_PROVIDER_IDS = exports.RUNTIME_QUOTA_PROVIDER_IDS = exports.QUOTA_PROVIDER_IDS = exports.MODEL_PROVIDER_IDS = exports.ACCOUNT_PROVIDER_IDS = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.parseMcpToolName = exports.parseLocalDateKey = exports.formatTokenCount = exports.formatLocalDateKey = exports.formatDurationMs = exports.addLocalDays = exports.projectSessionTranscript = exports.estimateSerializedTokens = exports.estimateTextTokens = exports.TOKEN_TOTAL_LABEL = exports.TOKEN_CONTEXT_LABEL = exports.sumTokenTotals = exports.summarizeTokens = exports.calculateNormalizedUsageCost = exports.extractNormalizedUsage = exports.normalizeProviderUsage = exports.DEFAULT_CONTEXT_WINDOW = exports.resolveModelContextWindow = exports.getModelContextWindowSize = void 0;
13
- exports.diagnoseProviderFailure = exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.sessionSummaryFromStats = exports.removeSessionSummary = exports.markFileImported = exports.isFileImported = exports.applySessionSummary = exports.weekKey = exports.usageBucketKey = exports.summarizeUsageRows = exports.bucketUsage = exports.findActiveBillingBlock = exports.computeBillingBlocks = exports.BILLING_BLOCK_DURATION_MS = void 0;
12
+ exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.resolveModelPricing = exports.getModelPricing = exports.parseModelId = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.SESSION_PROVIDER_IDS = exports.RUNTIME_QUOTA_PROVIDER_IDS = exports.QUOTA_PROVIDER_IDS = exports.MODEL_PROVIDER_IDS = exports.ACCOUNT_PROVIDER_IDS = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.parseMcpToolName = exports.parseLocalDateKey = exports.formatTokenCount = exports.formatLocalDateKey = exports.formatDurationMs = exports.addLocalDays = exports.projectSessionTranscript = exports.estimateSerializedTokens = exports.estimateTextTokens = exports.SYNTHETIC_MODEL = exports.ClaudeUsageDeduper = exports.TOKEN_TOTAL_LABEL = exports.TOKEN_SUBAGENTS_LABEL = exports.TOKEN_SESSION_TOTAL_LABEL = exports.TOKEN_MAIN_THREAD_LABEL = exports.TOKEN_CONTEXT_LABEL = exports.sumTokenTotals = exports.summarizeTokens = exports.formatTokenBreakdown = exports.calculateNormalizedUsageCost = exports.extractNormalizedUsage = exports.normalizeProviderUsage = exports.DEFAULT_CONTEXT_WINDOW = exports.resolveModelContextWindow = exports.getModelContextWindowSize = void 0;
13
+ exports.diagnoseProviderFailure = exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.sessionSummaryFromStats = exports.removeSessionSummary = exports.markFileImported = exports.isFileImported = exports.applySessionSummary = exports.weekKey = exports.usageBucketKey = exports.summarizeUsageRows = exports.bucketUsage = exports.findActiveBillingBlock = exports.computeBillingBlocks = exports.BILLING_BLOCK_DURATION_MS = exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = exports.resolveModelAlias = exports.registerModelAlias = exports.importResolvedModelCatalog = exports.exportResolvedModelCatalog = exports.formatCost = void 0;
14
14
  var modelContext_1 = require("./modelContext");
15
15
  Object.defineProperty(exports, "getModelContextWindowSize", { enumerable: true, get: function () { return modelContext_1.getModelContextWindowSize; } });
16
16
  Object.defineProperty(exports, "resolveModelContextWindow", { enumerable: true, get: function () { return modelContext_1.resolveModelContextWindow; } });
@@ -20,10 +20,17 @@ Object.defineProperty(exports, "normalizeProviderUsage", { enumerable: true, get
20
20
  Object.defineProperty(exports, "extractNormalizedUsage", { enumerable: true, get: function () { return usageNormalization_1.extractNormalizedUsage; } });
21
21
  Object.defineProperty(exports, "calculateNormalizedUsageCost", { enumerable: true, get: function () { return usageNormalization_1.calculateNormalizedUsageCost; } });
22
22
  var tokenSummary_1 = require("./tokenSummary");
23
+ Object.defineProperty(exports, "formatTokenBreakdown", { enumerable: true, get: function () { return tokenSummary_1.formatTokenBreakdown; } });
23
24
  Object.defineProperty(exports, "summarizeTokens", { enumerable: true, get: function () { return tokenSummary_1.summarizeTokens; } });
24
25
  Object.defineProperty(exports, "sumTokenTotals", { enumerable: true, get: function () { return tokenSummary_1.sumTokenTotals; } });
25
26
  Object.defineProperty(exports, "TOKEN_CONTEXT_LABEL", { enumerable: true, get: function () { return tokenSummary_1.TOKEN_CONTEXT_LABEL; } });
27
+ Object.defineProperty(exports, "TOKEN_MAIN_THREAD_LABEL", { enumerable: true, get: function () { return tokenSummary_1.TOKEN_MAIN_THREAD_LABEL; } });
28
+ Object.defineProperty(exports, "TOKEN_SESSION_TOTAL_LABEL", { enumerable: true, get: function () { return tokenSummary_1.TOKEN_SESSION_TOTAL_LABEL; } });
29
+ Object.defineProperty(exports, "TOKEN_SUBAGENTS_LABEL", { enumerable: true, get: function () { return tokenSummary_1.TOKEN_SUBAGENTS_LABEL; } });
26
30
  Object.defineProperty(exports, "TOKEN_TOTAL_LABEL", { enumerable: true, get: function () { return tokenSummary_1.TOKEN_TOTAL_LABEL; } });
31
+ var claudeUsageDedupe_1 = require("./usage/claudeUsageDedupe");
32
+ Object.defineProperty(exports, "ClaudeUsageDeduper", { enumerable: true, get: function () { return claudeUsageDedupe_1.ClaudeUsageDeduper; } });
33
+ Object.defineProperty(exports, "SYNTHETIC_MODEL", { enumerable: true, get: function () { return claudeUsageDedupe_1.SYNTHETIC_MODEL; } });
27
34
  var tokenEstimation_1 = require("./tokenEstimation");
28
35
  Object.defineProperty(exports, "estimateTextTokens", { enumerable: true, get: function () { return tokenEstimation_1.estimateTextTokens; } });
29
36
  Object.defineProperty(exports, "estimateSerializedTokens", { enumerable: true, get: function () { return tokenEstimation_1.estimateSerializedTokens; } });
@@ -39,5 +39,6 @@ function extractTokenUsage(event) {
39
39
  timestamp: new Date(event.timestamp),
40
40
  reportedCost: usage.reported_cost,
41
41
  reasoningTokens: usage.reasoning_tokens,
42
+ ...(event.message?.usageKind ? { usageKind: event.message.usageKind } : {}),
42
43
  };
43
44
  }
@@ -7,6 +7,7 @@
7
7
  * @module formatters/sessionDump
8
8
  */
9
9
  import type { AggregatedMetrics } from '../aggregation/types';
10
+ import type { SubagentStats } from '../types/sessionEvent';
10
11
  /** Options for text and markdown formatters. */
11
12
  export interface SessionDumpOptions {
12
13
  /** Terminal width for text output (default 120). */
@@ -15,11 +16,18 @@ export interface SessionDumpOptions {
15
16
  expand?: boolean;
16
17
  /** Session file name to display in markdown summary table. */
17
18
  sessionFileName?: string;
19
+ /**
20
+ * The session's subagents (`provider.scanSubagents()`). Their tokens are
21
+ * reported beside the main thread and added to the session total.
22
+ */
23
+ subagents?: readonly SubagentStats[];
18
24
  }
19
25
  /**
20
- * Serialize aggregated metrics as pretty-printed JSON.
26
+ * Serialize aggregated metrics as pretty-printed JSON, plus a `tokenSummary`
27
+ * block in the shared vocabulary: `mainThread`, per-subagent summaries,
28
+ * `subagentTotal`, and `combined` (main thread plus subagents).
21
29
  */
22
- export declare function formatSessionJson(metrics: AggregatedMetrics): string;
30
+ export declare function formatSessionJson(metrics: AggregatedMetrics, options?: Pick<SessionDumpOptions, 'subagents'>): string;
23
31
  /**
24
32
  * Format aggregated metrics as a plain-text report.
25
33
  */