sidekick-shared 0.27.1 → 0.27.3
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.
- package/CHANGELOG.md +46 -0
- package/README.md +35 -8
- package/dist/aggregation/EventAggregator.d.ts +12 -2
- package/dist/aggregation/EventAggregator.js +49 -19
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +9 -2
- package/dist/extractors/tokenUsage.js +1 -0
- package/dist/formatters/sessionDump.d.ts +10 -2
- package/dist/formatters/sessionDump.js +45 -23
- package/dist/index.d.ts +8 -4
- package/dist/index.js +31 -10
- package/dist/node.d.ts +3 -1
- package/dist/node.js +8 -2
- package/dist/parsers/codexParser.d.ts +22 -2
- package/dist/parsers/codexParser.js +64 -5
- package/dist/parsers/subagentScanner.d.ts +9 -0
- package/dist/parsers/subagentScanner.js +45 -12
- package/dist/parsers/subagentStatsCache.d.ts +17 -0
- package/dist/parsers/subagentStatsCache.js +82 -0
- package/dist/parsers/subagentTraceParser.js +19 -10
- package/dist/promptHistory.d.ts +82 -10
- package/dist/promptHistory.js +654 -183
- package/dist/providers/claudeCode.js +8 -41
- package/dist/providers/codex.js +28 -14
- package/dist/providers/codexDatabase.d.ts +16 -1
- package/dist/providers/codexDatabase.js +49 -1
- package/dist/providers/openCode.js +30 -4
- package/dist/report/htmlReportGenerator.js +33 -11
- package/dist/report/sessionReportInputs.d.ts +3 -1
- package/dist/report/sessionReportInputs.js +2 -0
- package/dist/report/types.d.ts +5 -0
- package/dist/schemas/sessionEvent.d.ts +30 -6
- package/dist/schemas/sessionEvent.js +7 -1
- package/dist/schemas/stateFile.d.ts +2 -0
- package/dist/schemas/stateFile.js +1 -0
- package/dist/sessionTokenTotals.d.ts +46 -0
- package/dist/sessionTokenTotals.js +138 -0
- package/dist/stateFile.d.ts +7 -0
- package/dist/tokenSummary.d.ts +27 -1
- package/dist/tokenSummary.js +50 -3
- package/dist/types/codex.d.ts +4 -0
- package/dist/types/sessionEvent.d.ts +22 -0
- package/dist/usage/claudeUsageDedupe.d.ts +53 -0
- package/dist/usage/claudeUsageDedupe.js +195 -0
- package/dist/usage/subagentUsage.d.ts +13 -0
- package/dist/usage/subagentUsage.js +21 -0
- package/dist/usage/usageEvents.d.ts +11 -2
- package/dist/usage/usageEvents.js +79 -8
- package/dist/watchers/eventBridge.js +5 -0
- package/dist/watchers/jsonlWatcher.d.ts +5 -0
- package/dist/watchers/jsonlWatcher.js +40 -5
- package/dist/watchers/types.d.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ 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.3] - 2026-09-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- 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`.
|
|
13
|
+
- 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.
|
|
14
|
+
- `sessionMessageSchema` accepts `stop_reason: null` (normalized to absent); such lines used to fail validation and drop the whole event.
|
|
15
|
+
- `<synthetic>` Claude messages carry no usage, so they add no call, unpriced model row, or heuristic compaction.
|
|
16
|
+
- 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`.
|
|
17
|
+
- `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.
|
|
18
|
+
- Timeline `metadata.tokenCount` and the HTML report's per-message tokens are cache-inclusive totals instead of input + output.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `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.
|
|
23
|
+
- `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.
|
|
24
|
+
- `formatSessionText`/`formatSessionMarkdown`/`formatSessionJson` and `HtmlReportOptions` accept `subagents`; the JSON dump adds `tokenSummary` (`mainThread`, `subagents`, `subagentTotal`, `combined`). `readSessionReportInputs()` returns `subagents`.
|
|
25
|
+
- `usageKind` on `SessionMessage`, `TokenUsage` (from `extractTokenUsage`), and `FollowEvent`; `StateFileContext.totalTokens` (optional); `CodexDbThread.source`, `agent_nickname`, `agent_role`.
|
|
26
|
+
- 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`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `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.
|
|
31
|
+
- `USAGE_CACHE_VERSION` is 2 and `SNAPSHOT_SCHEMA_VERSION` is 6, so caches built with the old counting are re-read.
|
|
32
|
+
- 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".
|
|
33
|
+
|
|
34
|
+
## [0.27.2] - 2026-09-22
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- `collectPromptHistory()` streams session files instead of reading each unread range into one buffer. A file whose unread part exceeded `maxFileBytes` used to be skipped on every call; now it is read in 1 MiB chunks up to `maxFileBytes` per call, stops after a complete line, keeps what it returned, and continues on the next call (cursor `backlog: true`), even if its size and mtime are unchanged. Memory stays near one chunk plus one record. Deadline, abort, `maxSessions`, `maxTotalBytes`, and the new `maxEntries` stop at the last complete line the same way instead of discarding the partly read file. The first file of a call always finishes its first record, so a record larger than the budget cannot stall. Lines split only at `\n`, so UTF-8 characters across chunk edges decode exactly; half-written final lines and bytes appended during a call wait for the next call.
|
|
39
|
+
- Late answer metadata no longer depends on a replay window. `pending` now keeps `lineBytes`, `timestamp`, and `answer: { status, id?, model?, usage? }` (answer state, never text), the scan continues from the file's offset however far the answer is, and only the prompt's own line is re-read (and verified against its length and timestamp) when the prompt must be returned again. Replays that 0.27.1 dropped for exceeding `maxFileBytes` no longer happen.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- Bounds `maxRecordBytes` (default 16 MiB; a longer record is skipped as it streams past, without buffering, even across calls while it is still being written) and `maxEntries` (default 10 000), and bound name `'maxEntries'`.
|
|
44
|
+
- `PromptHistoryResult.hasMore` (a bound left bytes or files unread) and `exclusions: PromptHistoryExclusion[]` (`{ provider, sessionId, reason: 'oversizedRecord' | 'malformedRecord', offset }`, up to 1000 per call). These are the only permanent exclusions; a prompt inside such a record is lost and consumes no ordinal.
|
|
45
|
+
- Stats `sessionsWithBacklog`, `sessionsRewritten`, `recordsOverSizeLimit`, and `recordsMalformed`; types `PromptHistoryAnswerState` and `PromptHistoryExclusion`.
|
|
46
|
+
- Rewrite detection: a file that shrank below its offset, whose first 4 KiB digest (`head`) changed, whose resume point no longer follows a newline, or whose pending prompt line no longer matches is read again from the start with fresh ordinals.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- The cursor is `version: 3`: sessions gain optional `backlog`, `skipping`, and `head`; `pending` gains `lineBytes`, `timestamp`, and `answer`; the cursor gains `next`, the key of the file a call-wide stop left off at, where the next call starts. Version 1 and 2 cursors are accepted: each file is re-verified once, and a version 2 `pending` prompt is replayed from its line in resumable passes. Files 0.27.1 skipped for size were never in its cursor and are read in full; metadata 0.27.1 already abandoned is not recoverable. A session carried unchanged from an older cursor and not visited keeps its old shape (for example `pending` without `answer`).
|
|
51
|
+
- `maxFileBytes` is now a per-file, per-call read budget instead of a skip threshold, and `stats.filesOverSizeLimit` counts files that used it up and continue on the next call.
|
|
52
|
+
- The Claude first-answer fallback id for assistant records without `message.id` is the line's byte offset instead of a per-scan line index, so it stays stable across calls.
|
|
53
|
+
|
|
8
54
|
## [0.27.1] - 2026-09-22
|
|
9
55
|
|
|
10
56
|
### 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.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.
|
|
13
|
+
- **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.
|
|
12
14
|
- **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.
|
|
13
15
|
- **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.
|
|
14
16
|
- **0.26.6: verified account switching** — `syncLiveAccountState()` reconciles the live Claude Code and Codex logins with saved profiles (registers unknown logins under their email, folds rotated tokens, merges duplicate Codex profiles, re-points the active pointer) and runs at startup, on account-change events, and as phase one of every switch; `getAccountHealth()` / `listAccountsWithHealth()` report secret-free `fresh | expiring | expired | unknown | missing` health; `switchAccount[Async]()` returns a `SwitchAccountResult` with verification, `runningConsumers`, `needsLogin`, and an `undoToken` for `undoLastSwitch()`; `detectRunningAccountConsumers()`, `getAccountLaunchEnv()`, `refreshInactiveAccounts()`, and `getCodexCredentialStoreMode()` are new. Claude logins run `claude auth login`, the Keychain suffix follows Claude Code 2.1.x, and the unused shell-hook helpers were removed.
|
|
@@ -99,14 +101,14 @@ returns the platform default that both overrides bypass.
|
|
|
99
101
|
| **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`) |
|
|
100
102
|
| **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
|
|
101
103
|
| **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
|
|
102
|
-
| **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting
|
|
104
|
+
| **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 |
|
|
103
105
|
| **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
|
|
104
106
|
| **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
|
|
105
107
|
| **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()`) |
|
|
106
108
|
| **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
|
|
107
109
|
| **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) |
|
|
108
110
|
| **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 |
|
|
109
|
-
| **Report** | Self-contained HTML session report generation
|
|
111
|
+
| **Report** | Self-contained HTML session report generation; `readSessionReportInputs()` returns metrics, transcript, and `subagents` for the session total |
|
|
110
112
|
| **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows) |
|
|
111
113
|
| **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 |
|
|
112
114
|
| **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()`) |
|
|
@@ -116,7 +118,7 @@ returns the platform default that both overrides bypass.
|
|
|
116
118
|
| **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 |
|
|
117
119
|
| **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 |
|
|
118
120
|
| **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 |
|
|
119
|
-
| **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
|
|
121
|
+
| **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 |
|
|
120
122
|
| **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 |
|
|
121
123
|
| **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()` |
|
|
122
124
|
| **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 |
|
|
@@ -271,6 +273,26 @@ for (const provider of providers) {
|
|
|
271
273
|
|
|
272
274
|
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.
|
|
273
275
|
|
|
276
|
+
`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):
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import {
|
|
280
|
+
ClaudeCodeProvider,
|
|
281
|
+
collectSessionTokenTotals,
|
|
282
|
+
formatTokenBreakdown,
|
|
283
|
+
formatTokenCount,
|
|
284
|
+
TOKEN_SESSION_TOTAL_LABEL,
|
|
285
|
+
} from 'sidekick-shared';
|
|
286
|
+
|
|
287
|
+
const provider = new ClaudeCodeProvider();
|
|
288
|
+
const totals = collectSessionTokenTotals(provider, '/path/to/session.jsonl');
|
|
289
|
+
// { mainThread, subagents: [{ agentId, agentType?, description?, summary }], subagentTotal, combined }
|
|
290
|
+
console.log(TOKEN_SESSION_TOTAL_LABEL, formatTokenBreakdown(totals.combined, formatTokenCount));
|
|
291
|
+
for (const agent of totals.subagents) console.log(agent.agentId, agent.summary.total);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
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`.
|
|
295
|
+
|
|
274
296
|
### Read canonical session history
|
|
275
297
|
|
|
276
298
|
```typescript
|
|
@@ -310,8 +332,10 @@ const result = await collectPromptHistory({
|
|
|
310
332
|
cursor,
|
|
311
333
|
bounds: {
|
|
312
334
|
maxSessions: 1000,
|
|
313
|
-
maxFileBytes: 16 << 20,
|
|
335
|
+
maxFileBytes: 16 << 20, // per file per call; the rest is read next call
|
|
314
336
|
maxTotalBytes: 256 << 20,
|
|
337
|
+
maxRecordBytes: 16 << 20, // one longer record is skipped and reported
|
|
338
|
+
maxEntries: 10_000,
|
|
315
339
|
deadlineMs: 30_000,
|
|
316
340
|
},
|
|
317
341
|
signal: AbortSignal.timeout(60_000),
|
|
@@ -321,16 +345,19 @@ for (const entry of result.entries) {
|
|
|
321
345
|
// { provider, sessionId, ordinal, timestamp, text, cwd, gitBranch?, model?, usage?, metadataStatus }
|
|
322
346
|
// Upsert by (provider, sessionId, ordinal): a prompt comes back when its answer metadata changes.
|
|
323
347
|
}
|
|
324
|
-
saveCursor(result.cursor); // sensitive local state; unchanged files are skipped next time
|
|
325
|
-
if (result.
|
|
348
|
+
saveCursor(result.cursor); // sensitive local state; fully read unchanged files are skipped next time
|
|
349
|
+
if (result.hasMore) scheduleAnotherCall(); // a bound stopped the pass; the cursor resumes it
|
|
350
|
+
for (const exclusion of result.exclusions) console.warn('record left out', exclusion); // permanent
|
|
326
351
|
```
|
|
327
352
|
|
|
328
353
|
- **Scope fails closed.** A prompt is returned only when its recorded working directory (the Claude line's `cwd`; the Codex `session_meta` / `turn_context` cwd) realpath-resolves to a root or somewhere inside one. Missing or unresolvable cwds are excluded and counted in `stats.promptsOutOfScope`; Codex scope never comes from the rollout's location. `cwd` is the only path in entries. Cursor keys hold no paths, but cursor values can (Codex resume state keeps the session's cwd and branch), so store the cursor as sensitive, local-only state.
|
|
329
354
|
- **Parse-only.** No redaction and no network I/O; text is untruncated.
|
|
330
355
|
- **Ordinals are stable.** Every human prompt in a file consumes one ordinal, even when it is out of scope, older than `since`, or lacks a timestamp (such lines are skipped and counted, never given a synthesized time). Entries are ordered by provider, session, then ordinal.
|
|
331
356
|
- **Model and usage** come from the first model call that answered the prompt (Claude: the first non-synthetic assistant message, with split records of that call replacing rather than adding usage; Codex: the first `token_count` with the model from `turn_context`). Later tool-loop calls never replace it. `inputTokens` is uncached input, and missing usage stays `undefined`, never zero.
|
|
332
|
-
- **Late answers enrich the same prompt.** A prompt can be collected before its answer is written. `metadataStatus` is `pending` (no answering call yet), `provisional` (Claude call still being written, `stop_reason: null`), or `final`. While a file's last prompt is not final, the cursor keeps the byte offset of its line and
|
|
333
|
-
- **
|
|
357
|
+
- **Late answers enrich the same prompt.** A prompt can be collected before its answer is written. `metadataStatus` is `pending` (no answering call yet), `provisional` (Claude call still being written, `stop_reason: null`), or `final`. While a file's last prompt is not final, the cursor keeps the byte offset and length of its line, its timestamp, and its answer state so far (first-answer message id, model, and usage, never prompt or answer text). The next call continues from where the file stopped, however far the answer is, and returns the prompt again with the same provider, sessionId, ordinal, timestamp, and text only when its model, usage, or status changed; the text comes from re-reading that one line. A prompt closed by the next prompt without an answer comes back once as `final` with no usage. Consumers should upsert by identity.
|
|
358
|
+
- **Large files are streamed and resumable.** Files are read in 1 MiB chunks and every pass stops after a complete line, so memory stays near one chunk plus one record whatever the file size. `maxFileBytes` is the most one file may read in one call: a file with more left keeps what it returned, records `backlog` in its cursor, is counted in `stats.filesOverSizeLimit`, and continues on the next call even if its size and mtime have not changed. `maxSessions`, `maxTotalBytes`, `maxEntries`, the deadline, and an abort stop the whole call the same way, and `cursor.next` makes the next call start with the file that was stopped, so a large or busy session cannot starve the others. The first file of a call always finishes its first record, so a record larger than the budget still moves forward. Bytes appended during a call and a half-written final line wait for the next call. `boundsHit` lists these resumable stops and `hasMore` says whether calling again would read more. No bound throws.
|
|
359
|
+
- **Permanent exclusions are reported.** A record over `maxRecordBytes` is skipped as it streams past without being buffered, even when it is still being written, and a complete line that is not JSON is skipped. Each is counted (`stats.recordsOverSizeLimit`, `stats.recordsMalformed`) and listed in `exclusions` with its byte offset. A prompt inside such a record is lost and does not consume an ordinal. A file that shrank or whose first 4 KiB changed is treated as rewritten: it is read again from the start with fresh ordinals and counted in `stats.sessionsRewritten`.
|
|
360
|
+
- **Cursor versions.** Output is `version: 3`. Cursors from 0.27.0 (`version: 1`) and 0.27.1 (`version: 2`) are accepted: each file is re-verified once, and a 0.27.1 pending prompt is replayed from its line in resumable passes, so it now settles even when its answer is far away. Files 0.27.1 skipped for size were never in its cursor and are read in full on the first call. Metadata that 0.27.1 already gave up on (a pending replay that exceeded `maxFileBytes`) was not saved and cannot be recovered, and a 0.27.0 cursor still cannot recover prompts it returned without metadata.
|
|
334
361
|
|
|
335
362
|
For an already-open incremental Codex rollout, keep one `CodexRolloutParser` per stream and pass each parsed `SessionEvent[]` batch to `projectSessionTranscript()` (or append the events to an existing projection). This preserves parser state for paired tool calls, model context, and cumulative token deltas; consumers should not convert raw `response_item` rows themselves.
|
|
336
363
|
|
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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 +=
|
|
666
|
+
this.reportedCalls += callIncrement;
|
|
660
667
|
}
|
|
661
668
|
else if (pricedUsage.source === 'unpriced') {
|
|
662
|
-
this.unpricedCalls +=
|
|
669
|
+
this.unpricedCalls += callIncrement;
|
|
663
670
|
}
|
|
664
671
|
else {
|
|
665
672
|
this.estimatedCostUsd += cost;
|
|
666
|
-
this.estimatedCalls +=
|
|
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
|
-
|
|
710
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
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 =
|
|
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.
|
|
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; } });
|
|
@@ -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
|
*/
|
|
@@ -18,12 +18,19 @@ exports.formatDuration = formatDuration;
|
|
|
18
18
|
const costProvenance_1 = require("../aggregation/costProvenance");
|
|
19
19
|
const formatting_1 = require("../formatting");
|
|
20
20
|
const tokenSummary_1 = require("../tokenSummary");
|
|
21
|
+
const sessionTokenTotals_1 = require("../sessionTokenTotals");
|
|
22
|
+
/** Main thread (the aggregated transcript) plus the session's subagents. */
|
|
23
|
+
function sessionTokens(metrics, options) {
|
|
24
|
+
return (0, sessionTokenTotals_1.combineSessionTokenTotals)(metrics.tokens, options.subagents ?? []);
|
|
25
|
+
}
|
|
21
26
|
// ── JSON output ──
|
|
22
27
|
/**
|
|
23
|
-
* Serialize aggregated metrics as pretty-printed JSON
|
|
28
|
+
* Serialize aggregated metrics as pretty-printed JSON, plus a `tokenSummary`
|
|
29
|
+
* block in the shared vocabulary: `mainThread`, per-subagent summaries,
|
|
30
|
+
* `subagentTotal`, and `combined` (main thread plus subagents).
|
|
24
31
|
*/
|
|
25
|
-
function formatSessionJson(metrics) {
|
|
26
|
-
return JSON.stringify(metrics, null, 2) + '\n';
|
|
32
|
+
function formatSessionJson(metrics, options = {}) {
|
|
33
|
+
return (JSON.stringify({ ...metrics, tokenSummary: sessionTokens(metrics, options) }, null, 2) + '\n');
|
|
27
34
|
}
|
|
28
35
|
// ── Text output ──
|
|
29
36
|
/**
|
|
@@ -40,13 +47,13 @@ function formatSessionText(metrics, options = {}) {
|
|
|
40
47
|
lines.push(hr);
|
|
41
48
|
lines.push('');
|
|
42
49
|
// Token summary
|
|
43
|
-
lines.push(formatTokenSummary(metrics));
|
|
50
|
+
lines.push(...formatTokenSummary(metrics, sessionTokens(metrics, options)));
|
|
44
51
|
lines.push('');
|
|
45
52
|
// Model stats
|
|
46
53
|
if (metrics.modelStats.length > 0) {
|
|
47
54
|
lines.push('Models:');
|
|
48
55
|
for (const m of metrics.modelStats) {
|
|
49
|
-
lines.push(` ${m.model}: ${m.calls} calls, ${fmtTokens(m.tokens)}
|
|
56
|
+
lines.push(` ${m.model}: ${m.calls} calls, ${fmtTokens(m.tokens)} total incl. cache, ${fmtCost(m.cost)}`);
|
|
50
57
|
}
|
|
51
58
|
lines.push('');
|
|
52
59
|
}
|
|
@@ -70,7 +77,7 @@ function formatSessionText(metrics, options = {}) {
|
|
|
70
77
|
parts.push(`${metrics.compactionCount} compaction(s)`);
|
|
71
78
|
if (metrics.truncationCount > 0)
|
|
72
79
|
parts.push(`${metrics.truncationCount} truncation(s)`);
|
|
73
|
-
lines.push(`Context: ${parts.join(', ')}`);
|
|
80
|
+
lines.push(`Context management: ${parts.join(', ')}`);
|
|
74
81
|
lines.push('');
|
|
75
82
|
}
|
|
76
83
|
// Timeline
|
|
@@ -130,7 +137,18 @@ function formatSessionMarkdown(metrics, options = {}) {
|
|
|
130
137
|
lines.push(`| Output | ${fmtTokens(metrics.tokens.outputTokens)} |`);
|
|
131
138
|
lines.push(`| Cache write | ${fmtTokens(metrics.tokens.cacheWriteTokens)} |`);
|
|
132
139
|
lines.push(`| Cache read | ${fmtTokens(metrics.tokens.cacheReadTokens)} |`);
|
|
133
|
-
|
|
140
|
+
const tokens = sessionTokens(metrics, options);
|
|
141
|
+
if (tokens.mainThread.billedOutsideBuckets > 0) {
|
|
142
|
+
lines.push(`| Reasoning (billed separately) | ${fmtTokens(tokens.mainThread.billedOutsideBuckets)} |`);
|
|
143
|
+
}
|
|
144
|
+
if (tokens.subagents.length > 0) {
|
|
145
|
+
lines.push(`| ${tokenSummary_1.TOKEN_MAIN_THREAD_LABEL} — ${tokenSummary_1.TOKEN_TOTAL_LABEL} | ${fmtTokens(tokens.mainThread.total)} |`);
|
|
146
|
+
lines.push(`| ${tokenSummary_1.TOKEN_SUBAGENTS_LABEL} (${tokens.subagents.length}) — ${tokenSummary_1.TOKEN_TOTAL_LABEL} | ${fmtTokens(tokens.subagentTotal.total)} |`);
|
|
147
|
+
lines.push(`| **${tokenSummary_1.TOKEN_SESSION_TOTAL_LABEL}** | **${fmtTokens(tokens.combined.total)}** |`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
lines.push(`| ${tokenSummary_1.TOKEN_TOTAL_LABEL} | ${fmtTokens(tokens.mainThread.total)} |`);
|
|
151
|
+
}
|
|
134
152
|
if (metrics.tokens.costUsd > 0) {
|
|
135
153
|
lines.push(`| Cost | ${fmtCost(metrics.tokens.costUsd)} (${(0, costProvenance_1.describeCostProvenance)(metrics.tokens)}) |`);
|
|
136
154
|
}
|
|
@@ -142,7 +160,7 @@ function formatSessionMarkdown(metrics, options = {}) {
|
|
|
142
160
|
if (metrics.modelStats.length > 0) {
|
|
143
161
|
lines.push('## Models');
|
|
144
162
|
lines.push('');
|
|
145
|
-
lines.push(`| Model | Calls |
|
|
163
|
+
lines.push(`| Model | Calls | ${tokenSummary_1.TOKEN_TOTAL_LABEL} | Cost |`);
|
|
146
164
|
lines.push(`|-------|-------|--------|------|`);
|
|
147
165
|
for (const m of metrics.modelStats) {
|
|
148
166
|
lines.push(`| ${m.model} | ${m.calls} | ${fmtTokens(m.tokens)} | ${fmtCost(m.cost)} |`);
|
|
@@ -190,6 +208,16 @@ function formatSessionMarkdown(metrics, options = {}) {
|
|
|
190
208
|
}
|
|
191
209
|
lines.push('');
|
|
192
210
|
}
|
|
211
|
+
if (tokens.subagents.length > 0) {
|
|
212
|
+
lines.push('## Subagent Tokens');
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(`| Agent | Type | Description | ${tokenSummary_1.TOKEN_TOTAL_LABEL} |`);
|
|
215
|
+
lines.push(`|-------|------|-------------|--------|`);
|
|
216
|
+
for (const agent of tokens.subagents) {
|
|
217
|
+
lines.push(`| ${agent.agentId} | ${agent.agentType ?? '-'} | ${agent.description ?? '-'} | ${fmtTokens(agent.summary.total)} |`);
|
|
218
|
+
}
|
|
219
|
+
lines.push('');
|
|
220
|
+
}
|
|
193
221
|
// Timeline
|
|
194
222
|
lines.push('## Timeline');
|
|
195
223
|
lines.push('');
|
|
@@ -225,23 +253,17 @@ function formatHeader(metrics) {
|
|
|
225
253
|
parts.push(`${metrics.eventCount} events`);
|
|
226
254
|
return parts.join(' | ');
|
|
227
255
|
}
|
|
228
|
-
function formatTokenSummary(metrics) {
|
|
256
|
+
function formatTokenSummary(metrics, tokens) {
|
|
229
257
|
const t = metrics.tokens;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
`
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
parts.push(`${fmtTokens(t.cacheReadTokens)} cache-read`);
|
|
238
|
-
if (t.cacheWriteTokens > 0)
|
|
239
|
-
parts.push(`${fmtTokens(t.cacheWriteTokens)} cache-write`);
|
|
240
|
-
const line = parts.join(', ');
|
|
241
|
-
if (t.costUsd > 0) {
|
|
242
|
-
return `${line}) Cost: ${fmtCost(t.costUsd)} (${(0, costProvenance_1.describeCostProvenance)(t)})`;
|
|
258
|
+
let first = `Tokens: ${(0, tokenSummary_1.formatTokenBreakdown)(tokens.mainThread, fmtTokens)}`;
|
|
259
|
+
if (t.costUsd > 0)
|
|
260
|
+
first += ` Cost: ${fmtCost(t.costUsd)} (${(0, costProvenance_1.describeCostProvenance)(t)})`;
|
|
261
|
+
const lines = [first];
|
|
262
|
+
if (tokens.subagents.length > 0) {
|
|
263
|
+
lines.push(`${tokenSummary_1.TOKEN_SUBAGENTS_LABEL} (${tokens.subagents.length}): ${(0, tokenSummary_1.formatTokenBreakdown)(tokens.subagentTotal, fmtTokens)}`);
|
|
264
|
+
lines.push(`${tokenSummary_1.TOKEN_SESSION_TOTAL_LABEL}: ${fmtTokens(tokens.combined.total)}`);
|
|
243
265
|
}
|
|
244
|
-
return
|
|
266
|
+
return lines;
|
|
245
267
|
}
|
|
246
268
|
/** @internal Exported for tests. */
|
|
247
269
|
function fmtTokens(n) {
|