sidekick-shared 0.24.5 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +205 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +170 -0
  7. package/dist/accountRegistry.d.ts +25 -2
  8. package/dist/accountRegistry.js +41 -13
  9. package/dist/accountStatus.d.ts +9 -1
  10. package/dist/accountStatus.js +3 -3
  11. package/dist/accounts.d.ts +3 -1
  12. package/dist/accounts.js +84 -64
  13. package/dist/aggregation/EventAggregator.d.ts +15 -2
  14. package/dist/aggregation/EventAggregator.js +54 -9
  15. package/dist/aggregation/costProvenance.d.ts +14 -0
  16. package/dist/aggregation/costProvenance.js +35 -0
  17. package/dist/aggregation/snapshot.d.ts +13 -2
  18. package/dist/aggregation/snapshot.js +57 -6
  19. package/dist/aggregation/types.d.ts +30 -1
  20. package/dist/analytics/compactionLedger.js +15 -5
  21. package/dist/analytics/qualityScore.js +2 -1
  22. package/dist/browser.d.ts +19 -4
  23. package/dist/browser.js +43 -1
  24. package/dist/codexProfiles.d.ts +3 -1
  25. package/dist/codexProfiles.js +4 -2
  26. package/dist/codexQuota.d.ts +2 -0
  27. package/dist/codexQuota.js +11 -66
  28. package/dist/codexQuotaWatcher.d.ts +6 -0
  29. package/dist/codexQuotaWatcher.js +101 -47
  30. package/dist/failingTools.d.ts +37 -0
  31. package/dist/failingTools.js +70 -0
  32. package/dist/formatters/sessionDump.js +13 -7
  33. package/dist/formatting.d.ts +13 -0
  34. package/dist/formatting.js +35 -0
  35. package/dist/historicalStore.d.ts +47 -0
  36. package/dist/historicalStore.js +312 -0
  37. package/dist/index.d.ts +67 -23
  38. package/dist/index.js +138 -26
  39. package/dist/modelAliases.d.ts +13 -0
  40. package/dist/modelAliases.js +58 -0
  41. package/dist/modelCatalog.d.ts +32 -0
  42. package/dist/modelCatalog.js +145 -0
  43. package/dist/modelContext.d.ts +36 -3
  44. package/dist/modelContext.js +109 -34
  45. package/dist/modelInfo.d.ts +30 -1
  46. package/dist/modelInfo.js +116 -26
  47. package/dist/multiProviderQuotaService.d.ts +8 -0
  48. package/dist/multiProviderQuotaService.js +64 -3
  49. package/dist/node.d.ts +23 -4
  50. package/dist/node.js +50 -1
  51. package/dist/observedContextWindows.d.ts +7 -0
  52. package/dist/observedContextWindows.js +17 -2
  53. package/dist/observedSessionCollector.d.ts +72 -6
  54. package/dist/observedSessionCollector.js +440 -47
  55. package/dist/parsers/codexHistory.d.ts +4 -2
  56. package/dist/parsers/codexHistory.js +9 -2
  57. package/dist/parsers/sessionActivityDetector.d.ts +28 -0
  58. package/dist/parsers/sessionActivityDetector.js +110 -15
  59. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  60. package/dist/parsers/sessionPathResolver.js +13 -10
  61. package/dist/peakHours.d.ts +34 -3
  62. package/dist/peakHours.js +92 -7
  63. package/dist/pricingCatalog.d.ts +7 -0
  64. package/dist/pricingCatalog.js +8 -5
  65. package/dist/providerIds.d.ts +11 -0
  66. package/dist/providerIds.js +9 -0
  67. package/dist/providerQuota.d.ts +3 -1
  68. package/dist/providerQuota.js +3 -0
  69. package/dist/providers/claudeCode.d.ts +10 -1
  70. package/dist/providers/claudeCode.js +211 -131
  71. package/dist/providers/codex.d.ts +20 -1
  72. package/dist/providers/codex.js +308 -244
  73. package/dist/providers/codexDatabase.d.ts +9 -0
  74. package/dist/providers/codexDatabase.js +107 -2
  75. package/dist/providers/detect.d.ts +2 -0
  76. package/dist/providers/detect.js +29 -24
  77. package/dist/providers/diagnostics.d.ts +13 -0
  78. package/dist/providers/diagnostics.js +88 -0
  79. package/dist/providers/factory.d.ts +20 -0
  80. package/dist/providers/factory.js +57 -0
  81. package/dist/providers/openCode.d.ts +22 -1
  82. package/dist/providers/openCode.js +460 -126
  83. package/dist/providers/openCodeDatabase.d.ts +10 -0
  84. package/dist/providers/openCodeDatabase.js +69 -0
  85. package/dist/providers/rolloutWalker.d.ts +53 -0
  86. package/dist/providers/rolloutWalker.js +209 -0
  87. package/dist/providers/types.d.ts +82 -2
  88. package/dist/providers/types.js +4 -0
  89. package/dist/quota.d.ts +34 -4
  90. package/dist/quota.js +37 -9
  91. package/dist/quotaHistory.d.ts +3 -2
  92. package/dist/quotaHistory.js +36 -35
  93. package/dist/quotaPoller.d.ts +13 -1
  94. package/dist/quotaPoller.js +65 -3
  95. package/dist/quotaResolver.d.ts +81 -0
  96. package/dist/quotaResolver.js +213 -0
  97. package/dist/quotaSnapshots.d.ts +15 -2
  98. package/dist/quotaSnapshots.js +25 -3
  99. package/dist/quotaThresholds.d.ts +60 -0
  100. package/dist/quotaThresholds.js +90 -0
  101. package/dist/report/htmlReportGenerator.js +10 -4
  102. package/dist/report/sessionReportInputs.d.ts +23 -0
  103. package/dist/report/sessionReportInputs.js +34 -0
  104. package/dist/schemas/accountManager.d.ts +2 -2
  105. package/dist/schemas/accountManager.js +2 -4
  106. package/dist/schemas/accountStatus.d.ts +3 -0
  107. package/dist/schemas/accountStatus.js +1 -0
  108. package/dist/schemas/index.d.ts +3 -0
  109. package/dist/schemas/index.js +16 -2
  110. package/dist/schemas/observedSessionV1.d.ts +27 -26
  111. package/dist/schemas/observedSessionV1.js +6 -4
  112. package/dist/schemas/quota.d.ts +141 -2
  113. package/dist/schemas/quota.js +20 -8
  114. package/dist/schemas/quotaHistory.d.ts +3 -2
  115. package/dist/schemas/quotaHistory.js +1 -1
  116. package/dist/schemas/sessionEvent.d.ts +6 -0
  117. package/dist/schemas/sessionEvent.js +1 -0
  118. package/dist/schemas/stateFile.d.ts +198 -0
  119. package/dist/schemas/stateFile.js +77 -0
  120. package/dist/schemas/tokenEstimation.d.ts +1 -1
  121. package/dist/schemas/transcript.js +2 -0
  122. package/dist/sessionFingerprint.d.ts +25 -0
  123. package/dist/sessionFingerprint.js +46 -0
  124. package/dist/sessionMonitor.d.ts +15 -0
  125. package/dist/sessionMonitor.js +142 -0
  126. package/dist/sessionPreviews.d.ts +33 -2
  127. package/dist/sessionPreviews.js +333 -6
  128. package/dist/sessionStats.d.ts +51 -0
  129. package/dist/sessionStats.js +166 -0
  130. package/dist/stateFile.d.ts +113 -0
  131. package/dist/stateFile.js +160 -0
  132. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  133. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  134. package/dist/statusline/formatter.d.ts +13 -0
  135. package/dist/statusline/formatter.js +48 -6
  136. package/dist/statusline/index.d.ts +10 -2
  137. package/dist/statusline/index.js +19 -1
  138. package/dist/tokenSummary.d.ts +62 -0
  139. package/dist/tokenSummary.js +72 -0
  140. package/dist/transcript.d.ts +5 -0
  141. package/dist/transcript.js +2 -0
  142. package/dist/types/observedSessionV1.d.ts +8 -2
  143. package/dist/types/observedSessionV1.js +88 -25
  144. package/dist/types/sessionEvent.d.ts +2 -0
  145. package/dist/usage/billingBlocks.d.ts +85 -0
  146. package/dist/usage/billingBlocks.js +153 -0
  147. package/dist/usage/importSessionHistory.d.ts +53 -0
  148. package/dist/usage/importSessionHistory.js +88 -0
  149. package/dist/usage/usageEvents.d.ts +92 -0
  150. package/dist/usage/usageEvents.js +322 -0
  151. package/dist/usage/usageReports.d.ts +76 -0
  152. package/dist/usage/usageReports.js +208 -0
  153. package/dist/watchers/factory.d.ts +6 -0
  154. package/dist/watchers/factory.js +30 -25
  155. package/dist/zaiQuotaApi.d.ts +2 -0
  156. package/dist/zaiQuotaApi.js +2 -0
  157. package/package.json +22 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,98 @@ 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.26.0] - 2026-09-05
9
+
10
+ ### Added
11
+
12
+ - `summarizeTokens()`, `sumTokenTotals()`, `TOKEN_TOTAL_LABEL`, and `TOKEN_CONTEXT_LABEL` define the one token vocabulary every Sidekick surface uses: `total` is input + output + cache writes + cache reads, `context` is input + cache; browser-safe
13
+ - `AggregatedTokens` gains `costUsd`, `costProvenance` (`reported` | `estimated` | `mixed` | `unpriced` | `none`), `reportedCostUsd`, `estimatedCostUsd`, and `unpricedCalls`; `describeCostProvenance()` and `classifyCostProvenance()` are exported; `CanonicalSessionTranscript.usage` adds `pricedCostUsd` and `unpricedEvents` (schema updated)
14
+ - `parseClaudeStatuslinePayload()` and `quotaFromStatuslinePayload()` (also on the `sidekick-shared/statusline` subpath, which now exports `writeQuotaSnapshot`, `appendQuotaHistorySample`, and `getWorkspaceIdFromPath`) read the JSON Claude Code pipes to a status-line command; `QuotaState.source` and quota-history samples accept `statusline`
15
+ - `formatStatusline({ live })` renders context %, session cost, prompt-cache hit rate, the seven-day window, and the snapshot age when it is older than five minutes
16
+ - `readQuotaSnapshot()` reports `ageMs` and `freshness`; `classifyQuotaFreshness()`, `formatQuotaAge()`, `QUOTA_FRESH_MAX_AGE_MS`, and `QUOTA_AGING_MAX_AGE_MS` are exported and the quota schema accepts the new fields
17
+ - `getScheduledPeakHoursState()`, `PEAK_HOURS_DESCRIPTION`, `PEAK_HOURS_CACHE_MS`, and `PeakHoursState.source` (`promoclock` | `schedule`); `fetchPeakHoursStatus({ force })` bypasses the ten-minute cache
18
+ - `evaluateQuotaThresholds()`, `describeQuotaThresholdAlert()`, and `DEFAULT_QUOTA_THRESHOLDS` for once-per-reset-window threshold alerts
19
+ - `formatLocalDateKey()`, `parseLocalDateKey()`, and `addLocalDays()` (root and browser entry points); `hydratePricingCatalog({ offline })`; `pruneSnapshots()` and `MAX_SNAPSHOT_FILES`; `getActiveAccountStatus(error, { selfHeal })`, mirrored on `resolveActiveClaudeAccount()` and `resolveActiveCodexAccount()`; `_resetProviderDetectionCache()` and `_resetPeakHoursCache()` for tests
20
+ - `resolveQuota({ providerId, workspacePath, preferFresh, allowApi, … })` resolves Claude, Codex, or z.ai quota with one precedence — a persisted sample younger than `QUOTA_FRESH_MAX_AGE_MS` (any origin), then session logs, then the provider API (persisted on success), then an aging or stale sample with the API failure attached — returning `ResolvedQuota` with `resolution`, `source`, `capturedSource`, `freshness`, and `ageMs`; every side effect is injectable
21
+ - `QuotaState.capturedSource` (`api` | `session` | `statusline`) records where a cached sample came from; `readQuotaSnapshot()` fills it and `writeQuotaSnapshot()` preserves it instead of storing `cache`; the quota schema accepts it; `QuotaSnapshotRecord`, `enrichCodexQuota()`, and `enrichZaiQuota()` are exported; `fetchQuota(token, { fetchImpl })` accepts an injected fetch
22
+ - `computeSessionFileStats(events, options)` and `readSessionFileStats(provider, sessionPath, { resolveLabel })` compute `SessionFileStats` for every provider over the shared `EventAggregator`; `firstUserPrompt(events)` derives a session label from events already in memory. `SessionFileStats` gains `availability` (`full` | `partial` | `unavailable`), `unavailableReason`, `costUsd`, `costProvenance`, `unpricedCalls`, and `toolFailures`; `reportedCost` is deprecated (same value as `costUsd`)
23
+ - `collectUsageEvents({ providers, since, until, workspacePath })` reads every session touched in a window once through its provider reader and returns priced usage events, caching raw token counts per session under `<configDir>/usage-cache/` keyed by size and mtime (priced at load, so a catalog refresh never leaves stale costs); `pruneUsageCache()`, `getUsageCacheDir()`, `USAGE_CACHE_VERSION`, and `MAX_USAGE_CACHE_FILES` are exported
24
+ - `computeBillingBlocks(events, { now })` and `findActiveBillingBlock()` (root and browser entry points) group usage events into ccusage-style five-hour blocks aligned to the UTC hour, with cache-inclusive totals, cost provenance, per-model usage, burn rate, remaining time, and end-of-block projections; `BILLING_BLOCK_DURATION_MS` is exported
25
+ - `bucketUsage(events, { granularity, utc, weekStartsOn, groupBy })`, `summarizeUsageRows()`, `usageBucketKey()`, and `weekKey()` (root and browser entry points) bucket usage events by event time into day, week, month, or session rows with provider, project, and model dimensions, cache-inclusive totals, and cost provenance
26
+ - `applySessionSummary(store, summary)`, `removeSessionSummary()`, `markFileImported()`, `isFileImported()`, and `sessionSummaryFromStats()` (root and browser entry points) are the pure `historical-data.json` mutations ported from the VS Code extension, so the CLI and the extension credit sessions identically; `importSessionHistory({ providers, since, isImported, applySummary, markImported })` reads every finished session once through `readSessionFileStats()` and hands summaries to the caller's store, skipping files modified within `ACTIVE_SESSION_MTIME_THRESHOLD_MS`
27
+ - `SessionFileStats.modelUsage` entries gain `costUsd` and `priced`
28
+ - `classifySessionActivity({ events, mtimeMs })` classifies a session from events already in memory with the same states, reasons, grace period, and staleness rule as `detectSessionActivity()`; `refreshSessionActivityState()` accepts the previous reason so a session that was active only by grace period ends once the grace period lapses; `getObservedActivityReason()` exposes the reason behind an observed session's activity; `SESSION_ACTIVITY_GRACE_PERIOD_MS` and `SESSION_ACTIVITY_STALENESS_MS` are exported
29
+ - `readSessionReportInputs(provider, sessionPath)` reads a session once and returns its events, aggregator metrics, and report transcript; `resolveSessionPath(provider, workspacePath, sessionId?)` is the session lookup `createWatcher()` uses (exact id, unique prefix, or most recent); `sessionFingerprintParts()` and `fingerprintString()` are exported alongside `fileFingerprintParts()`; `SessionMessage.stop_reason` is preserved by the event schema
30
+ - `walkRolloutFiles(roots, { maxDepth, maxFiles, limit, sessionId })` and `walkRolloutFilesAsync()` are the one capped walker for Codex rollout files: newest-dated directories first, each file stat'ed once, results newest first with size and session id, an early exit for `limit`, and defaults of six levels and 20 000 files; `findSessionFilesWithStats()` returns a Claude Code session directory's files with the stat the listing already paid for
31
+ - `writeStateFile()`, `readStateFile()`, `getStateFilePath()`, `quotaToStateFile()`, `billingBlockToStateFile()`, `STATE_FILE_SCHEMA_VERSION`, and the `SidekickStateFile` types (root, node, and `statusline` entry points) maintain the public, versioned `<configDir>/state.json` for external tools — account, quota windows with freshness, context usage, session cost, active billing block — writing atomically only when the content changed; `sidekickStateFileSchema` and its sub-schemas are exported from `sidekick-shared/schemas`
32
+ - `SessionPreviewListResult.hasMore` reports whether `listSessionPreviewsAsync()` left candidates beyond `limit`, so callers can offer a larger limit only when one would show more
33
+ - `mergeFailingToolWindows()`, `failingToolTrend()`, and `FAILING_TOOL_TREND_ARROWS` (root and `browser` entry points) merge the 7-day and 30-day `getTopFailingTools()` windows into per-tool rows with a trend, shared by `sidekick stats` and the VS Code Health tab
34
+
35
+ ### Changed
36
+
37
+ - The HTML report and session dump label cost by provenance and render unpriced sessions as `—` with the unpriced call count; their "Total" row is cache-inclusive and labelled "Total (incl. cache)"
38
+ - `calculateCompactionLedger()` prices re-establishment through `calculateNormalizedUsageCost()` instead of the deprecated additive-reasoning path
39
+ - Codex `readSessionStats()` computes context as uncached + cached input, matching `CodexProvider.computeContextSize()`
40
+ - OpenCode `workspaceMatches` only matches sessions started in the workspace or a subdirectory of it
41
+ - `readQuotaHistoryDailyBuckets()` keys buckets by local calendar day
42
+ - `detectProvider()` and `getAllDetectedProviders()` share one implementation memoised for five seconds
43
+ - `loadObservedContextWindows()` ignores entries older than `OBSERVED_CONTEXT_WINDOW_TTL_MS` (30 days)
44
+ - `saveSnapshot()` writes through the fsynced atomic writer and prunes the directory to the newest 200 files every 25 saves; quota-history append and prune run under a per-file cross-process lock with fsync; the pricing catalog cache is written atomically
45
+ - `DEFAULT_PEAK_HOURS_TIMEOUT_MS` is 4 000 ms; `fetchPeakHoursStatus()` falls back to the schedule rather than returning `unavailable: true`
46
+ - `listSessionPreviews({ since })` throws `RangeError` on an unparseable cutoff
47
+ - `AggregatedTokens.reportedCost` is deprecated (same value as `costUsd`); `SNAPSHOT_SCHEMA_VERSION` is 5
48
+ - Claude Code, Codex, and OpenCode `readSessionStats()` share one implementation: per-model `tokens` are cache-inclusive (`summarizeTokens().total`) for every provider, Claude sessions report catalog-estimated cost and a tool success/failure split instead of `reportedCost: 0`, OpenCode compaction and truncation counts come from the aggregator instead of hardcoded zeros, a missing or unreadable source reports `availability: 'unavailable'` with a reason instead of silent zeros, and the label is taken from the events already read (Codex and OpenCode still prefer their database title) so the file is never opened twice
49
+ - OpenCode's file-backed reader implements `seekTo()` by message count (its `getPosition()` unit), so a reader restored from a snapshot emits only newer messages instead of replaying the whole session; the database-backed reader's `exists()` checks the session row (memoised for 30 s, treating a transient query failure as present) instead of always returning true
50
+ - `createProviderSessionAdapterV1().read()` reads a session once: stats, transcript, activity, and the pending request all come from one flushed reader pass instead of three separate file reads, and activity is classified from the events, which also fixes Codex sessions (whose raw lines never matched the Claude-only tail patterns) and database-backed OpenCode sessions (which always read as ended). `watch()` checks the size/mtime fingerprint before re-reading and refreshes only the activity in place while the content is unchanged, so a 2 s poll on an idle session costs a `stat`
51
+ - `ObservedSessionCollector` cache refreshes demote a session that was active only by grace period to ended after five seconds, matching a fresh classification
52
+ - The Codex provider's six recursive walkers (session discovery, listing, id lookup, project folders, subagent scan) and the quota resolver's separate copy all use the capped walker; `getAllProjectFolders()` reads `session_meta` only for rollouts newer than the SQLite index's mtime when the index answered, and every discovery path uses the per-path cwd cache (now bounded to 5 000 entries) instead of re-reading the first line of every rollout; `scanSubagents()` stops after the 50 newest-dated rollouts instead of enumerating the whole history; the local Codex quota scan walks at most `maxSessionFiles` rollouts and no longer stats every path a second time to sort it; `ClaudeCodeProvider.listAllSessionFiles()` no longer stats every session twice
53
+
54
+ ### Fixed
55
+
56
+ - `onAccountsChanged()` applies a later subscriber's faster `pollIntervalMs` and resets the interval after the last unsubscribe
57
+ - `OpenCodeProvider.dispose()` resets `dbStatus`; `CodexProvider.listSessionFilesAsync()` records its filesystem fallback for `getLastOperationStatus()`
58
+ - Observed-session cache hits could report a session as active for up to five minutes when a fresh parse would have said ended (the 0.25.0 grace-period drift note)
59
+ - `addCurrentAccount()` and `removeAccount()` hold the registry lock across their whole read-modify-write (new `withSavedAccountRegistryLock()` and `replaceSavedAccountProfilesUnlocked()`), so a concurrent add from another process is no longer dropped when the Claude profile set is replaced
60
+ - `resolveActiveClaudeAccount()`'s self-heal of the active pointer no longer fires an accounts-changed notification from a read path (`setActiveSavedAccount(..., { silent: true })`)
61
+ - `readSessionPreview()` / `readSessionPreviewAsync()` honour `maxPrefixBytes: 0` as "skip the prefix scan" (`firstTimestamp` and `workspacePath` come back null, the file is not opened) instead of flooring the budget to one byte
62
+ - `createSessionProviders()` documents that `diagnostics` is live (updated in place as providers run) and should be copied for a snapshot
63
+ - The packaging contract tests create their scratch projects under the OS temp directory instead of inside the package, so an interrupted run cannot leave a recursive `node_modules/sidekick-shared` link behind
64
+
65
+ ## [0.25.0] - 2026-08-18
66
+
67
+ ### Added
68
+
69
+ - `listSessionPreviewsAsync()` and `readSessionPreviewAsync()` provide bounded-concurrency, cooperative session preview reads while preserving the synchronous APIs and their signatures
70
+ - Async preview labeling batches Codex and OpenCode database work into at most one `sqlite3` subprocess per provider per few hundred sessions listed (ids are chunked clear of argv limits); when `sqlite3` is absent, the same bounded file-scan fallback remains available
71
+ - `OpenCodeProvider.listAllSessionFiles()` completes native preview enumeration across Claude Code, Codex, and OpenCode
72
+ - `ObservedSessionCollector.subscribe()` and `SessionMonitor.subscribe()` emit debounced, coalesced session changes with the previous and current fingerprints, backed by filesystem watching plus a documented catch-up poll for unreliable filesystems
73
+ - `createSessionProviders({ onDiagnostic })` constructs every usable provider independently and reports unavailable providers without aborting the host
74
+ - `SessionProviderBase.findSessionById()` has provider-native implementations: filename lookup for Claude Code and Codex, and a database lookup with a bounded file-scan fallback for OpenCode
75
+ - `exportResolvedModelCatalog()` and `importResolvedModelCatalog()` transfer serializable context-window and pricing resolutions between Node and browser realms without private hooks
76
+ - `registerModelAlias()` lets hosts resolve short model names to canonical ids, and catalog provenance marks prefix-inherited context and pricing matches so callers can reject them
77
+ - Stable provider-id arrays are exported as runtime values alongside their derived TypeScript unions
78
+ - `onAccountsChanged()` reports process-local and filesystem-observed login, logout, and account-switch changes without account-state polling in each host
79
+
80
+ ### Changed
81
+
82
+ - Provider constructors are environment-independent and perform no filesystem, configuration, database, or binary probing; environmental failures are deferred to first use and returned as structured diagnostics
83
+ - Repeated identical provider degradations are coalesced in the factory diagnostic result, keeping first-use reporting bounded in long-lived hosts
84
+ - `ObservedSessionCollector.collect()` caches parsed sessions by fingerprint, so a second collection over unchanged files performs no content reads
85
+ - Observed sessions expose `fingerprintParts: { sizeBytes, mtimeMs }` alongside the compatible opaque fingerprint string
86
+ - Collector reads accept bounded-concurrency and cooperative-yield hooks; cached observations refresh `observedAt` and activity state while retaining `contentObservedAt` to make the age of cached usage/model data explicit
87
+ - `ObservedSessionDiagnostic` now includes `severity` and `phase` while retaining every existing `kind` value
88
+ - Account and quota entry points guarantee results that validate against the same-release exported schemas, including additive schema fields, and their contract tests enforce the boundary
89
+ - Every documented package subpath now declares explicit `types`, `import`, `require`, and `default` export conditions and resolves in both Vite and TypeScript's legacy `moduleResolution: node` mode without aliases
90
+ - `readCodexHistory()` retains raw epoch-second `ts`, adds normalized `tsMs`, and sizes its tail read from `limit` unless an explicit byte bound is supplied
91
+ - Observed-session projection accepts `observationOnly: true`, which reports transcripts as read-only observations with `capabilities.resume` disabled
92
+ - `QuotaPoller`, `MultiProviderQuotaService`, and `CodexQuotaWatcher` stay dormant when their account is absent and wake on account changes instead of issuing empty polls
93
+
94
+ ### Fixed
95
+
96
+ - Codex and OpenCode database-backed operations attach an availability diagnostic when the external `sqlite3` binary is missing, so an unavailable runtime is distinguishable from an empty workspace
97
+ - A wrong session id resolves to `null` through every provider instead of surfacing discovery or environment errors
98
+ - Exact model pricing/context entries are considered before prefix inheritance across all sources, and prefix-only matches are explicit in provenance rather than silently trusted as exact
99
+
8
100
  ## [0.24.5] - 2026-08-18
9
101
 
10
102
  ### Added
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
+ - **One token vocabulary and cost provenance (0.26.0)** — `summarizeTokens()` gives every surface the same `total` (input + output + cache writes + cache reads) and `context` (input + cache) figures; `AggregatedTokens.costUsd` carries `costProvenance` so reports can say whether a cost was provider-reported or estimated; `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` turn Claude Code's status-line JSON into an official `statusline` quota source; `readQuotaSnapshot()` reports `ageMs` / `freshness` and keeps the sample's origin as `capturedSource`; `resolveQuota()` is the one quota resolution path (fresh persisted sample → session logs → provider API → older sample) shared by the CLI, the MCP server, and both dashboards; `readSessionFileStats()` / `computeSessionFileStats()` give every provider one `readSessionStats()` over the shared aggregator, with `availability`, `costProvenance`, and `toolFailures` on `SessionFileStats`; `collectUsageEvents()` reads priced usage events from session logs with a per-session cache, `computeBillingBlocks()` groups them into five-hour billing blocks, and `bucketUsage()` turns them into day, week, month, or session rows; `applySessionSummary()` and `importSessionHistory()` let any host fold finished sessions into `historical-data.json` the way the extension does; observed sessions are read once per discovery, re-read only when their fingerprint changes, and classified from events by `classifySessionActivity()`; `readSessionReportInputs()` derives report metrics and transcript from one read; `writeStateFile()` / `readStateFile()` (also on the `statusline` subpath) maintain the public, versioned `state.json` for external tools, validated by `sidekickStateFileSchema`; `getScheduledPeakHoursState()` and `evaluateQuotaThresholds()` are shared by the CLI and extension dashboards.
13
+ - **Host-safe APIs (0.25.0)** — `listSessionPreviewsAsync()` / `readSessionPreviewAsync()` read previews with bounded concurrency and cooperative yielding; `ObservedSessionCollector.subscribe()` and `SessionMonitor.subscribe()` replace host polling loops with debounced change batches; `createSessionProviders({ onDiagnostic })` constructs every usable provider with structured diagnostics instead of throwing; `findSessionById()` resolves one session without scanning; `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` and `registerModelAlias()` transfer context/pricing resolutions across realms; `onAccountsChanged()` reports login/logout/switch events; provider constructors perform no environment I/O, and quota pollers stay dormant without an account.
12
14
  - **Native session-file helpers** — `encodeClaudeWorkspacePath()` / `getClaudeSessionDirectory()` locate Claude Code's `~/.claude/projects/` directories with the real on-disk encoding, `findCodexRolloutFile()` resolves a Codex session id to its rollout path, `readCodexHistory()` tail-reads `~/.codex/history.jsonl`, `listSessionPreviews()` / `readSessionPreview()` build a cheap stat-first recent-sessions index across providers, and `parseMcpToolName()` splits `mcp__<server>__<tool>` identifiers (browser-safe).
13
15
  - **Non-blocking account APIs & locked sync writers** — every login/switch entry point gains an async variant (`switchAccountAsync()`, `getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `prepareCodexAccountAsync()`, `finalizeCodexAccountAsync()`, `switchToCodexAccountAsync()`) that keeps `codex` CLI probes off the event loop, and synchronous callers get the same cross-process store lock via `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()`, with progress-based timeouts and abandoned-lock reclaim.
14
16
  - **Canonical usage and pricing** — provider-specific counters normalize into disjoint input/cache/output/reasoning categories with `normalizeProviderUsage()`; `calculateNormalizedUsageCost()` prevents cache/reasoning double counting and reports pricing provenance.
@@ -41,16 +43,19 @@ shelling out to an executable **`sqlite3` on `PATH`**. There is no bundled
41
43
  driver. `CodexProvider` shells out to the same binary, but only for an optional
42
44
  index.
43
45
 
44
- Without it `OpenCodeProvider` returns **empty results rather than an error**,
45
- which is indistinguishable from an empty workspace. Call `getRuntimeStatus()`,
46
- implemented by `OpenCodeProvider` only, to tell the two apart:
46
+ Without it, database-backed reads still return results rather than throwing —
47
+ but as of 0.25.0 the missing binary is no longer invisible: Codex and OpenCode
48
+ attach a `sqlite_missing` diagnostic to affected operations, so an unavailable
49
+ runtime is distinguishable from an empty workspace. Construct providers through
50
+ `createSessionProviders({ onDiagnostic })` to receive those diagnostics, or ask
51
+ any provider directly — `getRuntimeStatus()` is implemented by all three:
47
52
 
48
53
  ```typescript
49
54
  const status = provider.getRuntimeStatus?.();
50
55
  // status.kind: 'available' | 'db_missing' | 'sqlite_missing'
51
- // | 'sqlite_blocked' | 'query_failed'
56
+ // | 'sqlite_blocked' | 'query_failed' | ...
52
57
  if (status && status.kind !== 'available') {
53
- console.warn(`Session data unavailable: ${status.kind}`);
58
+ console.warn(`Session data degraded: ${status.kind}`);
54
59
  }
55
60
  ```
56
61
 
@@ -78,56 +83,56 @@ returns the platform default that both overrides bypass.
78
83
 
79
84
  ## API Overview
80
85
 
81
- | Module | Description |
82
- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
- | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
84
- | **Paths** | Config directory resolution, project data paths, workspace encoding |
85
- | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
86
- | **Writers** | Atomic persistence writers — `atomicWriteJson()` / `updateJsonStoreAtomic()` and their synchronous twins `atomicWriteJsonSync()` / `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()` (same cross-process lock file, progress-based timeouts, abandoned-lock reclaim), plus lock-coordinated capture helpers `addTask()`, `completeTask()`, `addDecision()`, `addNote()` that merge into the shared stores without clobbering concurrent writers |
87
- | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
88
- | **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`) |
89
- | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
90
- | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
91
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
92
- | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
93
- | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
94
- | **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()`) |
95
- | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
96
- | **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) |
97
- | **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 |
98
- | **Report** | Self-contained HTML session report generation |
99
- | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
100
- | **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 |
101
- | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
102
- | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` exposes the per-provider session probe |
103
- | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, quota, account status, and quota history — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
104
- | **Extractors** | Pure functions for single-event processing: `extractTokenUsage()`, `extractToolCall()` (top-level `tool_use`), `extractToolCalls()` (assistant content blocks), plus Node-only session asset extraction (`gatherAssetsForCwd()`, `readClaudeAssets()`, `readCodexAssets()`) for URLs, file paths, commands, and plans |
105
- | **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 / Opus 4.8 / Opus 4.7 / Sonnet 4.7 1M and GPT-5.x 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()`, and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
106
- | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
107
- | **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 |
108
- | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for first-run bootstrap of the active system Claude/Codex credentials as a "Default" saved account, `getActiveAccountStatus()` for a single-pass active-account read across providers, and live-first `resolveActiveClaudeAccount()` / `resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
109
- | **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. 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 |
110
- | **Terminal sync & auto-switch** | Opt-in terminal profile pointers, shell-hook/launcher helpers, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) that moves to a healthier saved account when quota crosses a configured threshold; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
111
- | **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 |
112
- | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
113
- | **Statusline** | Cache-only one-line account/quota/burn footer — `formatStatusline()`, `selectStatuslineAccount()`, `BurnRateCalculator`, and `estimateTimeToQuota()`, also published via the [`sidekick-shared/statusline`](#supported-import-paths) subpath for fast per-prompt rendering |
114
- | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
86
+ | Module | Description |
87
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
89
+ | **Paths** | Config directory resolution, project data paths, workspace encoding |
90
+ | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
91
+ | **Writers** | Atomic persistence writers — `atomicWriteJson()` / `updateJsonStoreAtomic()` and their synchronous twins `atomicWriteJsonSync()` / `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()` (same cross-process lock file, progress-based timeouts, abandoned-lock reclaim), plus lock-coordinated capture helpers `addTask()`, `completeTask()`, `addDecision()`, `addNote()` that merge into the shared stores without clobbering concurrent writers |
92
+ | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem. As of 0.25.0 constructors perform no environment I/O — build them all with `createSessionProviders({ onDiagnostic })`, which reports unavailable providers as structured diagnostics instead of failing; `findSessionById()` resolves one session through provider-native indexes; stable provider-id const arrays (`SESSION_PROVIDER_IDS`, `ACCOUNT_PROVIDER_IDS`, `QUOTA_PROVIDER_IDS`, `MODEL_PROVIDER_IDS`) accompany the derived unions |
93
+ | **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`) |
94
+ | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
95
+ | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
96
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
97
+ | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
98
+ | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
99
+ | **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()`) |
100
+ | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
101
+ | **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) |
102
+ | **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 |
103
+ | **Report** | Self-contained HTML session report generation |
104
+ | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
105
+ | **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 |
106
+ | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
107
+ | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` exposes the per-provider session probe |
108
+ | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, quota, account status, account management, quota history, and the public `state.json` contract (`sidekickStateFileSchema`) — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Account and quota APIs guarantee results valid against the same-release schemas. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
109
+ | **Extractors** | Pure functions for single-event processing: `extractTokenUsage()`, `extractToolCall()` (top-level `tool_use`), `extractToolCalls()` (assistant content blocks), plus Node-only session asset extraction (`gatherAssetsForCwd()`, `readClaudeAssets()`, `readCodexAssets()`) for URLs, file paths, commands, and plans |
110
+ | **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 / Opus 4.8 / Opus 4.7 / Sonnet 4.7 1M and GPT-5.x 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 |
111
+ | **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 |
112
+ | **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 |
113
+ | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for first-run bootstrap of the active system Claude/Codex credentials as a "Default" saved account, `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 |
114
+ | **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. 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 |
115
+ | **Terminal sync & auto-switch** | Opt-in terminal profile pointers, shell-hook/launcher helpers, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) that moves to a healthier saved account when quota crosses a configured threshold; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
116
+ | **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 |
117
+ | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
118
+ | **Statusline** | Cache-only one-line account/quota/burn footer — `formatStatusline()`, `selectStatuslineAccount()`, `BurnRateCalculator`, and `estimateTimeToQuota()`, also published via the [`sidekick-shared/statusline`](#supported-import-paths) subpath for fast per-prompt rendering |
119
+ | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
115
120
 
116
121
  ## Supported import paths
117
122
 
118
- `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime.
123
+ `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.
119
124
 
120
- | Path | Runtime | What it exposes |
121
- | ------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
123
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
124
- | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, observed context-window persistence, recent-session/transcript reads, session previews (`listSessionPreviews()` / `readSessionPreview()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
125
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
126
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
127
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
128
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
129
- | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, quota history) — fs-free, no Node builtins. |
130
- | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
125
+ | Path | Runtime | What it exposes |
126
+ | ------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
128
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
129
+ | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, observed context-window persistence, recent-session/transcript reads, session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
130
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
131
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
132
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
133
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
134
+ | `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. |
135
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
131
136
 
132
137
  ### Browser / webview runtimes
133
138
 
@@ -169,6 +174,31 @@ await loadObservedContextWindows();
169
174
 
170
175
  Both calls always resolve; a network failure, missing cache, or malformed store leaves lookups on the static tables. To record a window a provider reports at runtime, call `recordObservedContextWindow(modelId, window)` — it persists only when the value changes.
171
176
 
177
+ ### Transfer resolved model data across realms
178
+
179
+ A realm that can hydrate (extension host, CLI) can hand its resolved context
180
+ windows and pricing to one that cannot (webview, browser bundle) as one
181
+ serializable snapshot — no private hooks, no re-hydration:
182
+
183
+ ```typescript
184
+ // Node side (after hydratePricingCatalog / loadObservedContextWindows):
185
+ import { exportResolvedModelCatalog } from 'sidekick-shared';
186
+ const snapshot = exportResolvedModelCatalog(); // or pass specific model ids
187
+
188
+ // Browser side:
189
+ import { importResolvedModelCatalog, registerModelAlias } from 'sidekick-shared/browser';
190
+ const { imported, diagnostics } = importResolvedModelCatalog(snapshot);
191
+ ```
192
+
193
+ Entries carry provenance (`source`, `match`, `matchedModelId`, and
194
+ `inheritedByPrefix` for pricing), so consumers can reject prefix-inherited
195
+ guesses. Unresolved entries (`default` window / `none` pricing) are skipped on
196
+ import rather than pinned, successive partial imports merge, and imported data
197
+ ranks below anything the importing realm has observed first-hand.
198
+ `registerModelAlias('sonnet', 'claude-sonnet-5')` resolves short or
199
+ host-specific names everywhere lookups happen; data recorded under the alias id
200
+ itself keeps winning over the canonical fallback.
201
+
172
202
  ### Normalize and price provider usage
173
203
 
174
204
  ```typescript
@@ -209,6 +239,32 @@ const estimate = estimateTextTokens(sourceCode, {
209
239
 
210
240
  `sidekick-fallback-v1` counts Latin/source-code text at 3.5 characters per token, CJK code points at one token, emoji at two, and other non-ASCII code points at one. Empty input is zero. An injected finite, nonnegative exact result takes precedence.
211
241
 
242
+ ### Read session stats for any provider
243
+
244
+ ```typescript
245
+ import { createSessionProviders, readSessionFileStats } from 'sidekick-shared';
246
+
247
+ const { providers } = createSessionProviders();
248
+ for (const provider of providers) {
249
+ for (const sessionPath of provider.findAllSessions(process.cwd())) {
250
+ const stats = provider.readSessionStats(sessionPath); // same as readSessionFileStats(provider, sessionPath)
251
+ if (stats.availability === 'unavailable') {
252
+ console.warn(`${provider.id}: ${stats.unavailableReason}`);
253
+ continue;
254
+ }
255
+ console.log(
256
+ stats.label,
257
+ stats.modelUsage,
258
+ stats.costUsd,
259
+ stats.costProvenance,
260
+ stats.toolFailures,
261
+ );
262
+ }
263
+ }
264
+ ```
265
+
266
+ 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.
267
+
212
268
  ### Read canonical session history
213
269
 
214
270
  ```typescript
@@ -274,6 +330,17 @@ const previews = listSessionPreviews([new ClaudeCodeProvider(), new CodexProvide
274
330
 
275
331
  `readSessionPreview(provider, filePath)` reads the same bounded preview for one known session file — `null` when the file is missing or unreadable, `null` fields when its content is malformed.
276
332
 
333
+ Hosts with a UI loop should prefer the async variants from `sidekick-shared/node`. They read with bounded concurrency, yield between files, batch Codex/OpenCode label lookups into non-blocking `sqlite3` subprocesses, and — unlike their sync twins — return degradation status alongside the data:
334
+
335
+ ```typescript
336
+ import { listSessionPreviewsAsync, readSessionPreviewAsync } from 'sidekick-shared/node';
337
+
338
+ const { previews, diagnostics, hasMore } = await listSessionPreviewsAsync(providers, { limit: 10 });
339
+ const { preview } = await readSessionPreviewAsync(provider, filePath);
340
+ ```
341
+
342
+ To resolve one session id to its file without scanning, every bundled provider implements `findSessionById(workspacePath, sessionId)` — a filename join for Claude Code and Codex, a database lookup for OpenCode — returning `null` for misses instead of reading transcripts. (It is optional on the `SessionProviderBase` interface so pre-0.25 external implementations keep compiling; fall back to `findAllSessions()` when absent.)
343
+
277
344
  MCP tool identifiers (`mcp__<server>__<tool>`) split with `parseMcpToolName(name)` — first-double-underscore rule, `null` for malformed names, safe in browser bundles.
278
345
 
279
346
  ### Collect observed sessions with isolation and retry
@@ -299,10 +366,55 @@ applyReadOnlyProjection(source.capabilities);
299
366
 
300
367
  Discovery and reads are isolated, retries use bounded 30-second-to-5-minute exponential backoff, file changes bypass delay, duplicate identical failures are suppressed, and recovery emits a content-free diagnostic. Clock and fingerprint callbacks are injectable for deterministic hosts/tests.
301
368
 
369
+ As of 0.25.0, parses are cached by fingerprint — a second `collect()` over unchanged files performs no content reads (cache hits refresh `observedAt` and activity state; `contentObservedAt` reports how old the cached usage/model data actually is, and `cacheHit` marks them). Observations expose structured `fingerprintParts: { sizeBytes, mtimeMs }` alongside the opaque fingerprint string, and diagnostics carry `severity` and `phase` in addition to their existing `kind`.
370
+
371
+ Instead of a host polling loop, subscribe to debounced change batches. Each change carries the previous and current fingerprints; filesystem watching is combined with a catch-up poll for filesystems that drop events:
372
+
373
+ ```typescript
374
+ const subscription = collector.subscribe(
375
+ (batch) => {
376
+ for (const change of batch.changes) {
377
+ // change.type: 'added' | 'changed' | 'removed'
378
+ console.log(batch.providerId, change.type, change.reference.sessionId);
379
+ console.log(change.previousFingerprint, '→', change.fingerprint);
380
+ }
381
+ void collector.collect(); // consume changed sessions through the parse cache
382
+ },
383
+ { debounceMs: 250, pollIntervalMs: 30_000 },
384
+ );
385
+ // later: subscription.dispose();
386
+ ```
387
+
388
+ `SessionMonitor.subscribe()` offers the same push model at the session-event level.
389
+
302
390
  ## Usage Examples
303
391
 
392
+ ### Construct session providers for a long-lived host
393
+
394
+ `createSessionProviders()` builds every requested provider without probing the
395
+ filesystem or binaries — constructors are I/O-free as of 0.25.0, so one
396
+ unavailable provider cannot abort host startup. Environmental failures surface
397
+ on first use as structured diagnostics instead:
398
+
399
+ ```typescript
400
+ import { createSessionProviders } from 'sidekick-shared';
401
+
402
+ const { providers, diagnostics } = createSessionProviders({
403
+ onDiagnostic: (diagnostic) => {
404
+ // e.g. { providerId: 'opencode', kind: 'sqlite_missing',
405
+ // severity: 'warning', phase: 'query', message: '...' }
406
+ log(diagnostic);
407
+ },
408
+ });
409
+ ```
410
+
411
+ Repeated identical degradations are coalesced, so the diagnostic stream stays
412
+ bounded across long-lived polling and subscription cycles.
413
+
304
414
  ### Detect the active session provider
305
415
 
416
+ For a single-provider surface, auto-detect and construct just one:
417
+
306
418
  ```typescript
307
419
  import {
308
420
  detectProvider,
@@ -363,6 +475,25 @@ if (!peak.unavailable && peak.isPeak) {
363
475
  }
364
476
  ```
365
477
 
478
+ ### Resolve quota with one precedence
479
+
480
+ ```typescript
481
+ import { resolveQuota } from 'sidekick-shared';
482
+
483
+ // Fresh persisted sample (status line, session, or API; under five minutes) →
484
+ // session logs (Codex rollouts) → provider API → older sample labelled by age.
485
+ const quota = await resolveQuota({ providerId: 'claude-code' });
486
+ console.log(quota.resolution); // 'snapshot-fresh' | 'session' | 'api' | 'snapshot-aging' | 'snapshot-stale' | 'unavailable'
487
+ console.log(quota.source, quota.capturedSource, quota.freshness, quota.ageMs);
488
+
489
+ // `--refresh` semantics: ask the API first.
490
+ await resolveQuota({ providerId: 'codex', workspacePath: process.cwd(), preferFresh: false });
491
+ // Paint from local samples only (a poller will fetch live data next).
492
+ await resolveQuota({ providerId: 'claude-code', allowApi: false });
493
+ ```
494
+
495
+ `resolveQuota()` is what `sidekick quota`, `quota --all`, the MCP `get_quota_status` tool, and both dashboards call, so every surface reports the same numbers and the same `resolution`. Every side effect is injectable (`readSnapshot`, `writeSnapshot`, `fetchImpl`, `getClaudeAccessToken`, the account resolvers) for tests.
496
+
366
497
  ### Fetch subscription quota
367
498
 
368
499
  ```typescript
@@ -518,7 +649,7 @@ const parser = new JsonlParser(
518
649
  parser.processChunk(rawData);
519
650
  ```
520
651
 
521
- The boundary schemas — `sessionEventSchema` plus the quota, account-status, and quota-history schemas — are also importable fs-free from `sidekick-shared/schemas`, which keeps Zod out of bundles that only need the pure math/formatting helpers. `extractSessionEvents()` from the same subpath unwraps Claude Code `progress`-wrapped events into canonical `SessionEvent[]`.
652
+ The boundary schemas — `sessionEventSchema` plus the quota, account-status, account-management, and quota-history schemas — are also importable fs-free from `sidekick-shared/schemas`, which keeps Zod out of bundles that only need the pure math/formatting helpers. Account and quota APIs are guaranteed to return values valid against the schemas exported in the same release, so consumers do not need to parse those library results again. Additive account or quota fields are added to the matching exported schema in the same release. `extractSessionEvents()` from the same subpath unwraps Claude Code `progress`-wrapped events into canonical `SessionEvent[]`.
522
653
 
523
654
  ### Tail raw JSONL events incrementally
524
655
 
@@ -594,6 +725,28 @@ console.log(status.claude.present, status.claude.email);
594
725
  console.log(status.codex.present, status.codex.label);
595
726
  ```
596
727
 
728
+ To react to logins, logouts, and switches without polling that status yourself,
729
+ subscribe to `onAccountsChanged()`. It combines process-local mutation signals,
730
+ filesystem watches on the account stores, and a low-frequency catch-up poll —
731
+ and only emits when the status actually changed:
732
+
733
+ ```typescript
734
+ import { onAccountsChanged } from 'sidekick-shared';
735
+
736
+ const subscription = onAccountsChanged(
737
+ ({ reason, status }) => {
738
+ // reason: 'local' | 'filesystem' | 'poll'
739
+ render(status.claude, status.codex);
740
+ },
741
+ { emitCurrent: true },
742
+ );
743
+ // later: subscription.dispose();
744
+ ```
745
+
746
+ `QuotaPoller`, `MultiProviderQuotaService`, and `CodexQuotaWatcher` use the same
747
+ signal internally: they stay dormant while no matching account exists and wake
748
+ when one appears.
749
+
597
750
  For display surfaces that must reflect the **currently logged-in** account — even after a native `claude /login` or `codex login` outside Sidekick — use the live-first resolvers. They prefer the live provider auth (`~/.claude/.claude.json` oauthAccount; the `~/.codex/auth.json` id_token JWT) over the saved `activeByProvider` pointer, fall back to the registry, and self-heal the saved pointer on an unambiguous match (best-effort, never creating or deleting profiles):
598
751
 
599
752
  ```typescript
@@ -0,0 +1,3 @@
1
+ export type AccountChangeReason = 'local' | 'filesystem' | 'poll';
2
+ export declare function _onRawAccountsChanged(listener: (reason: AccountChangeReason) => void): () => void;
3
+ export declare function _emitAccountsChanged(reason: AccountChangeReason): void;