sidekick-shared 0.24.4 → 0.25.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 (98) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +200 -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 +151 -0
  7. package/dist/accountManager.d.ts +13 -0
  8. package/dist/accountManager.js +60 -32
  9. package/dist/accountRegistry.d.ts +3 -1
  10. package/dist/accountRegistry.js +64 -41
  11. package/dist/accounts.js +40 -19
  12. package/dist/autoSwitch.d.ts +2 -1
  13. package/dist/autoSwitch.js +32 -7
  14. package/dist/browser.d.ts +10 -3
  15. package/dist/browser.js +17 -1
  16. package/dist/codexProfiles.d.ts +14 -0
  17. package/dist/codexProfiles.js +268 -116
  18. package/dist/codexQuotaWatcher.d.ts +6 -0
  19. package/dist/codexQuotaWatcher.js +101 -47
  20. package/dist/context/sessionContext.js +2 -4
  21. package/dist/ensureDefaultAccounts.js +4 -4
  22. package/dist/index.d.ts +32 -15
  23. package/dist/index.js +54 -9
  24. package/dist/modelAliases.d.ts +13 -0
  25. package/dist/modelAliases.js +58 -0
  26. package/dist/modelCatalog.d.ts +32 -0
  27. package/dist/modelCatalog.js +145 -0
  28. package/dist/modelContext.d.ts +36 -3
  29. package/dist/modelContext.js +109 -34
  30. package/dist/modelInfo.d.ts +30 -1
  31. package/dist/modelInfo.js +116 -26
  32. package/dist/multiProviderQuotaService.d.ts +8 -0
  33. package/dist/multiProviderQuotaService.js +64 -3
  34. package/dist/node.d.ts +8 -2
  35. package/dist/node.js +11 -1
  36. package/dist/observedContextWindows.d.ts +13 -0
  37. package/dist/observedContextWindows.js +13 -0
  38. package/dist/observedSessionCollector.d.ts +75 -4
  39. package/dist/observedSessionCollector.js +452 -42
  40. package/dist/parsers/codexHistory.d.ts +32 -0
  41. package/dist/parsers/codexHistory.js +125 -0
  42. package/dist/parsers/mcpToolName.d.ts +26 -0
  43. package/dist/parsers/mcpToolName.js +31 -0
  44. package/dist/parsers/sessionActivityDetector.d.ts +6 -0
  45. package/dist/parsers/sessionActivityDetector.js +17 -0
  46. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  47. package/dist/parsers/sessionPathResolver.js +11 -4
  48. package/dist/paths.d.ts +7 -3
  49. package/dist/paths.js +7 -3
  50. package/dist/providerIds.d.ts +11 -0
  51. package/dist/providerIds.js +9 -0
  52. package/dist/providerQuota.d.ts +3 -1
  53. package/dist/providerQuota.js +3 -0
  54. package/dist/providers/claudeCode.d.ts +11 -1
  55. package/dist/providers/claudeCode.js +222 -39
  56. package/dist/providers/codex.d.ts +39 -1
  57. package/dist/providers/codex.js +326 -39
  58. package/dist/providers/codexDatabase.d.ts +9 -0
  59. package/dist/providers/codexDatabase.js +107 -2
  60. package/dist/providers/diagnostics.d.ts +13 -0
  61. package/dist/providers/diagnostics.js +88 -0
  62. package/dist/providers/factory.d.ts +14 -0
  63. package/dist/providers/factory.js +57 -0
  64. package/dist/providers/openCode.d.ts +20 -1
  65. package/dist/providers/openCode.js +361 -11
  66. package/dist/providers/openCodeDatabase.d.ts +10 -0
  67. package/dist/providers/openCodeDatabase.js +69 -0
  68. package/dist/providers/types.d.ts +57 -2
  69. package/dist/providers/types.js +4 -0
  70. package/dist/quota.d.ts +4 -1
  71. package/dist/quota.js +3 -7
  72. package/dist/quotaHistory.d.ts +3 -2
  73. package/dist/quotaHistory.js +2 -2
  74. package/dist/quotaPoller.d.ts +13 -1
  75. package/dist/quotaPoller.js +65 -3
  76. package/dist/quotaSnapshots.js +8 -77
  77. package/dist/schemas/accountManager.d.ts +2 -2
  78. package/dist/schemas/accountManager.js +2 -4
  79. package/dist/schemas/accountStatus.d.ts +3 -0
  80. package/dist/schemas/accountStatus.js +1 -0
  81. package/dist/schemas/index.d.ts +1 -0
  82. package/dist/schemas/index.js +7 -2
  83. package/dist/schemas/observedSessionV1.d.ts +27 -26
  84. package/dist/schemas/observedSessionV1.js +6 -4
  85. package/dist/schemas/quota.d.ts +2 -2
  86. package/dist/schemas/quota.js +3 -6
  87. package/dist/schemas/quotaHistory.d.ts +2 -2
  88. package/dist/schemas/tokenEstimation.d.ts +1 -1
  89. package/dist/sessionMonitor.d.ts +15 -0
  90. package/dist/sessionMonitor.js +142 -0
  91. package/dist/sessionPreviews.d.ts +85 -0
  92. package/dist/sessionPreviews.js +549 -0
  93. package/dist/sessionTranscripts.js +10 -4
  94. package/dist/types/observedSessionV1.d.ts +6 -2
  95. package/dist/types/observedSessionV1.js +14 -8
  96. package/dist/writers/atomic.d.ts +20 -0
  97. package/dist/writers/atomic.js +190 -19
  98. package/package.json +22 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,65 @@ 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.25.0] - 2026-08-18
9
+
10
+ ### Added
11
+
12
+ - `listSessionPreviewsAsync()` and `readSessionPreviewAsync()` provide bounded-concurrency, cooperative session preview reads while preserving the synchronous APIs and their signatures
13
+ - 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
14
+ - `OpenCodeProvider.listAllSessionFiles()` completes native preview enumeration across Claude Code, Codex, and OpenCode
15
+ - `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
16
+ - `createSessionProviders({ onDiagnostic })` constructs every usable provider independently and reports unavailable providers without aborting the host
17
+ - `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
18
+ - `exportResolvedModelCatalog()` and `importResolvedModelCatalog()` transfer serializable context-window and pricing resolutions between Node and browser realms without private hooks
19
+ - `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
20
+ - Stable provider-id arrays are exported as runtime values alongside their derived TypeScript unions
21
+ - `onAccountsChanged()` reports process-local and filesystem-observed login, logout, and account-switch changes without account-state polling in each host
22
+
23
+ ### Changed
24
+
25
+ - 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
26
+ - Repeated identical provider degradations are coalesced in the factory diagnostic result, keeping first-use reporting bounded in long-lived hosts
27
+ - `ObservedSessionCollector.collect()` caches parsed sessions by fingerprint, so a second collection over unchanged files performs no content reads
28
+ - Observed sessions expose `fingerprintParts: { sizeBytes, mtimeMs }` alongside the compatible opaque fingerprint string
29
+ - 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
30
+ - `ObservedSessionDiagnostic` now includes `severity` and `phase` while retaining every existing `kind` value
31
+ - 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
32
+ - 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
33
+ - `readCodexHistory()` retains raw epoch-second `ts`, adds normalized `tsMs`, and sizes its tail read from `limit` unless an explicit byte bound is supplied
34
+ - Observed-session projection accepts `observationOnly: true`, which reports transcripts as read-only observations with `capabilities.resume` disabled
35
+ - `QuotaPoller`, `MultiProviderQuotaService`, and `CodexQuotaWatcher` stay dormant when their account is absent and wake on account changes instead of issuing empty polls
36
+
37
+ ### Fixed
38
+
39
+ - 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
40
+ - A wrong session id resolves to `null` through every provider instead of surfacing discovery or environment errors
41
+ - 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
42
+
43
+ ## [0.24.5] - 2026-08-18
44
+
45
+ ### Added
46
+
47
+ - Session-preview index: `listSessionPreviews()` enumerates session files stat-first across providers, sorts by mtime, applies `since`/`limit`, and content-reads only the post-limit survivors — so cost tracks the size of the answer, not the size of the history. `readSessionPreview()` reads one file's bounded preview (label, first prompt, first timestamp, workspace path) and degrades to `null` fields instead of throwing. Both are exported from the root and `sidekick-shared/node`
48
+ - `readCodexHistory()` returns the newest entries of `~/.codex/history.jsonl` from a bounded tail read, dropping the leading partial line and tolerating multibyte characters straddling the boundary. `findCodexRolloutFile()` resolves a Codex session id to its `sessions/YYYY/MM/DD/rollout-*.jsonl` transcript by filename only, newest mtime winning across monitored Codex homes
49
+ - `parseMcpToolName()` is the canonical splitter for `mcp__<server>__<tool>` identifiers, exported browser-safe; the session-context projector's MCP-server inference delegates to it. The degenerate `mcp__server__` shape (empty tool part) now parses as `null` rather than yielding a server name
50
+ - Async account entry points: `getAccountLoginStatusAsync`, `finalizeAccountLoginAsync`, `switchAccountAsync`, `prepareCodexAccountAsync`, `finalizeCodexAccountAsync`, `switchToCodexAccountAsync`. Each runs the codex CLI probes through `execFile` so the caller's event loop stays free, and probes are resolved before any lock is taken — never while holding one. Sync entry points keep their exact signatures for CLI callers
51
+ - Synchronous locked writers: `withFileLockSync()`, `updateJsonStoreAtomicSync()`, and `atomicWriteFileSync()` share the async path's cross-process lock-file protocol (sync and async waiters exclude each other), with an absolute 15s wait ceiling because a sync waiter blocks its event loop
52
+ - `AutoSwitchController.switchAccount` accepts a Promise-returning callback and defaults to `switchAccountAsync`, so a threshold crossing no longer runs blocking codex probes on the caller's loop. Overlapping quota updates are skipped while a switch is in flight, and a switch that lands after `dispose()` records its cooldown state without emitting a stale transition
53
+ - `SessionProviderBase` gains an optional `listAllSessionFiles()` so providers can expose native session-file enumeration to the preview index
54
+
55
+ ### Fixed
56
+
57
+ - The store lock's timeout is measured from the last time the lock changed hands rather than from a waiter's first attempt, so a burst of concurrent writers no longer manufactures "Timed out waiting for store lock" once the queue outlives the flat budget — a wedged holder still trips it in 3s
58
+ - A lock whose heartbeat mtime has been frozen past two minutes is reclaimed even when its recorded PID probes alive: PID recycling made `process.kill(pid, 0)` an unreliable liveness signal, and a crashed owner with a recycled PID used to wedge the store until a human deleted the lock
59
+ - The quota-snapshot store drops its private copy of the lock — which still had the flat 15s-from-first-attempt bug — and delegates to the shared one, keeping the same lock path so mixed-version processes still exclude each other. Snapshot writes pick up the shared writer's fsync and directory sync
60
+ - Four account modules each carried their own lock-free atomic writer with a fixed `.tmp` suffix, so cross-process account writes were last-writer-wins and two processes could interleave the same temp file. All account writes go through the shared fsynced writers, registry read-modify-write cycles hold the registry lock, and live credential swaps hold a per-provider auth-swap lock so two switchers cannot interleave a stash/restore of a rotated Codex refresh token
61
+ - `switchToAccount()` and `applyActiveClaudeToLiveHome()` threw on a held auth-swap lock where the Codex twins return a failed `AccountManagerResult`; they now keep the non-throwing contract their callers rely on
62
+ - Codex login-status and `pgrep` probes no longer freeze the caller's event loop for up to 4s; `ensureDefaultAccounts` and `spawnAccountLogin` run on the async path
63
+ - `AutoSwitchController`'s fire-and-forget update handler could surface a throwing registry read, snapshot read, or consumer `onTransition` as an unhandled promise rejection — process-fatal on modern Node. Those errors are now caught and logged
64
+ - `readCodexHistory()` rejects entries whose `ts` is not a finite number — `JSON.parse` yields `Infinity` for out-of-range literals, which later RangeErrored out of `Date` serialization in consumers. `readSessionPreview()` likewise returns `null` for a file whose mtime cannot be serialized, per its degrade-don't-throw contract
65
+ - `listRecentSessions()` sorts before labeling, so per-file content reads no longer scale with total session count, and a corrupt session beyond the limit no longer poisons the whole call
66
+
8
67
  ## [0.24.4] - 2026-07-25
9
68
 
10
69
  ### Fixed
package/README.md CHANGED
@@ -9,6 +9,9 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
+ - **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.
13
+ - **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).
14
+ - **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.
12
15
  - **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.
13
16
  - **Transcripts and resilient collection** — `projectSessionTranscript()` is browser-safe; Node consumers can use `listRecentSessions()`, `readSessionTranscript()`, and `ObservedSessionCollector` across Claude Code, Codex, and OpenCode readers.
14
17
  - **Shared token estimation** — `estimateTextTokens()` / `estimateSerializedTokens()` provide an injectable exact-counter path and the stable tokenizer-free `sidekick-fallback-v1` heuristic.
@@ -39,16 +42,19 @@ shelling out to an executable **`sqlite3` on `PATH`**. There is no bundled
39
42
  driver. `CodexProvider` shells out to the same binary, but only for an optional
40
43
  index.
41
44
 
42
- Without it `OpenCodeProvider` returns **empty results rather than an error**,
43
- which is indistinguishable from an empty workspace. Call `getRuntimeStatus()`,
44
- implemented by `OpenCodeProvider` only, to tell the two apart:
45
+ Without it, database-backed reads still return results rather than throwing —
46
+ but as of 0.25.0 the missing binary is no longer invisible: Codex and OpenCode
47
+ attach a `sqlite_missing` diagnostic to affected operations, so an unavailable
48
+ runtime is distinguishable from an empty workspace. Construct providers through
49
+ `createSessionProviders({ onDiagnostic })` to receive those diagnostics, or ask
50
+ any provider directly — `getRuntimeStatus()` is implemented by all three:
45
51
 
46
52
  ```typescript
47
53
  const status = provider.getRuntimeStatus?.();
48
54
  // status.kind: 'available' | 'db_missing' | 'sqlite_missing'
49
- // | 'sqlite_blocked' | 'query_failed'
55
+ // | 'sqlite_blocked' | 'query_failed' | ...
50
56
  if (status && status.kind !== 'available') {
51
- console.warn(`Session data unavailable: ${status.kind}`);
57
+ console.warn(`Session data degraded: ${status.kind}`);
52
58
  }
53
59
  ```
54
60
 
@@ -76,56 +82,56 @@ returns the platform default that both overrides bypass.
76
82
 
77
83
  ## API Overview
78
84
 
79
- | Module | Description |
80
- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
- | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
82
- | **Paths** | Config directory resolution, project data paths, workspace encoding |
83
- | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
84
- | **Writers** | Atomic persistence writers — `atomicWriteJson()` / `updateJsonStoreAtomic()` plus lock-coordinated capture helpers `addTask()`, `completeTask()`, `addDecision()`, `addNote()` that merge into the shared stores without clobbering concurrent writers |
85
- | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
86
- | **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`) |
87
- | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
88
- | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
89
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
90
- | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
91
- | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
92
- | **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()`) |
93
- | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
94
- | **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) |
95
- | **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 |
96
- | **Report** | Self-contained HTML session report generation |
97
- | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
98
- | **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 |
99
- | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
100
- | **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 |
101
- | **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 |
102
- | **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 |
103
- | **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()`) |
104
- | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
105
- | **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 |
106
- | **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 |
107
- | **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. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
108
- | **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 |
109
- | **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 |
110
- | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
111
- | **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 |
112
- | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
85
+ | Module | Description |
86
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
88
+ | **Paths** | Config directory resolution, project data paths, workspace encoding |
89
+ | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
90
+ | **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 |
91
+ | **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 |
92
+ | **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`) |
93
+ | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
94
+ | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
95
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
96
+ | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
97
+ | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
98
+ | **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()`) |
99
+ | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
100
+ | **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) |
101
+ | **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 |
102
+ | **Report** | Self-contained HTML session report generation |
103
+ | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
104
+ | **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 |
105
+ | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
106
+ | **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 |
107
+ | **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, and quota history — 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 |
108
+ | **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 |
109
+ | **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 |
110
+ | **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 |
111
+ | **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 |
112
+ | **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 |
113
+ | **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 |
114
+ | **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()` |
115
+ | **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 |
116
+ | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
117
+ | **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 |
118
+ | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
113
119
 
114
120
  ## Supported import paths
115
121
 
116
- `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime.
122
+ `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.
117
123
 
118
- | Path | Runtime | What it exposes |
119
- | ------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
121
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math. |
122
- | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, observed context-window persistence, recent-session/transcript reads, and resilient observed-session collection (`fs` + providers). |
123
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
124
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
125
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
126
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
127
- | `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. |
128
- | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
124
+ | Path | Runtime | What it exposes |
125
+ | ------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
126
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
127
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
128
+ | `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). |
129
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
130
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
131
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
132
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
133
+ | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, account management, quota history) — fs-free, no Node builtins. |
134
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
129
135
 
130
136
  ### Browser / webview runtimes
131
137
 
@@ -167,6 +173,31 @@ await loadObservedContextWindows();
167
173
 
168
174
  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.
169
175
 
176
+ ### Transfer resolved model data across realms
177
+
178
+ A realm that can hydrate (extension host, CLI) can hand its resolved context
179
+ windows and pricing to one that cannot (webview, browser bundle) as one
180
+ serializable snapshot — no private hooks, no re-hydration:
181
+
182
+ ```typescript
183
+ // Node side (after hydratePricingCatalog / loadObservedContextWindows):
184
+ import { exportResolvedModelCatalog } from 'sidekick-shared';
185
+ const snapshot = exportResolvedModelCatalog(); // or pass specific model ids
186
+
187
+ // Browser side:
188
+ import { importResolvedModelCatalog, registerModelAlias } from 'sidekick-shared/browser';
189
+ const { imported, diagnostics } = importResolvedModelCatalog(snapshot);
190
+ ```
191
+
192
+ Entries carry provenance (`source`, `match`, `matchedModelId`, and
193
+ `inheritedByPrefix` for pricing), so consumers can reject prefix-inherited
194
+ guesses. Unresolved entries (`default` window / `none` pricing) are skipped on
195
+ import rather than pinned, successive partial imports merge, and imported data
196
+ ranks below anything the importing realm has observed first-hand.
197
+ `registerModelAlias('sonnet', 'claude-sonnet-5')` resolves short or
198
+ host-specific names everywhere lookups happen; data recorded under the alias id
199
+ itself keeps winning over the canonical fallback.
200
+
170
201
  ### Normalize and price provider usage
171
202
 
172
203
  ```typescript
@@ -235,6 +266,56 @@ const humanCliPrompts = transcript.messages.filter(
235
266
 
236
267
  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.
237
268
 
269
+ ### Locate provider-native session files
270
+
271
+ The library exports the same primitives its providers use to find session files on disk, so consumers never need to hand-roll path encoding or directory walks:
272
+
273
+ ```typescript
274
+ import {
275
+ getClaudeSessionDirectory, // ~/.claude/projects/<encoded workspace>
276
+ encodeClaudeWorkspacePath, // the encoding itself, if you need the raw string
277
+ discoverSessionDirectory, // mismatch-tolerant lookup — prefer this for reads
278
+ findAllClaudeSessions,
279
+ findCodexRolloutFile, // Codex session id -> rollout file path
280
+ readCodexHistory, // bounded tail of ~/.codex/history.jsonl, newest first
281
+ } from 'sidekick-shared';
282
+
283
+ const sessions = findAllClaudeSessions('/home/user/code/my-project');
284
+
285
+ const [latest] = readCodexHistory({ limit: 1 });
286
+ const rolloutPath = latest ? findCodexRolloutFile(latest.sessionId) : null;
287
+ ```
288
+
289
+ > **Naming trap:** the top-level `encodeWorkspacePath` export is Sidekick's own config-store slug (it keeps dots) and does **not** match Claude Code's directory scheme. For `~/.claude/projects/` paths always use `encodeClaudeWorkspacePath` — or better, `getClaudeSessionDirectory` / `discoverSessionDirectory`, which tolerate encoding drift.
290
+
291
+ For a cross-provider "recent sessions" surface, `listSessionPreviews()` enumerates stat-only, sorts by mtime, applies `since`/`limit`, and only then does bounded content reads for the survivors — so cost tracks the size of the answer, not the size of the history:
292
+
293
+ ```typescript
294
+ import { ClaudeCodeProvider, CodexProvider, listSessionPreviews } from 'sidekick-shared';
295
+
296
+ const previews = listSessionPreviews([new ClaudeCodeProvider(), new CodexProvider()], {
297
+ limit: 10,
298
+ since: lastRefreshTime, // optional incremental refresh
299
+ });
300
+ // -> [{ provider, sessionId, filePath, modifiedAt, sizeBytes,
301
+ // firstUserPrompt, firstTimestamp, workspacePath }, ...]
302
+ ```
303
+
304
+ `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.
305
+
306
+ 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:
307
+
308
+ ```typescript
309
+ import { listSessionPreviewsAsync, readSessionPreviewAsync } from 'sidekick-shared/node';
310
+
311
+ const { previews, diagnostics } = await listSessionPreviewsAsync(providers, { limit: 10 });
312
+ const { preview } = await readSessionPreviewAsync(provider, filePath);
313
+ ```
314
+
315
+ 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.)
316
+
317
+ MCP tool identifiers (`mcp__<server>__<tool>`) split with `parseMcpToolName(name)` — first-double-underscore rule, `null` for malformed names, safe in browser bundles.
318
+
238
319
  ### Collect observed sessions with isolation and retry
239
320
 
240
321
  ```typescript
@@ -258,10 +339,55 @@ applyReadOnlyProjection(source.capabilities);
258
339
 
259
340
  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.
260
341
 
342
+ 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`.
343
+
344
+ 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:
345
+
346
+ ```typescript
347
+ const subscription = collector.subscribe(
348
+ (batch) => {
349
+ for (const change of batch.changes) {
350
+ // change.type: 'added' | 'changed' | 'removed'
351
+ console.log(batch.providerId, change.type, change.reference.sessionId);
352
+ console.log(change.previousFingerprint, '→', change.fingerprint);
353
+ }
354
+ void collector.collect(); // consume changed sessions through the parse cache
355
+ },
356
+ { debounceMs: 250, pollIntervalMs: 30_000 },
357
+ );
358
+ // later: subscription.dispose();
359
+ ```
360
+
361
+ `SessionMonitor.subscribe()` offers the same push model at the session-event level.
362
+
261
363
  ## Usage Examples
262
364
 
365
+ ### Construct session providers for a long-lived host
366
+
367
+ `createSessionProviders()` builds every requested provider without probing the
368
+ filesystem or binaries — constructors are I/O-free as of 0.25.0, so one
369
+ unavailable provider cannot abort host startup. Environmental failures surface
370
+ on first use as structured diagnostics instead:
371
+
372
+ ```typescript
373
+ import { createSessionProviders } from 'sidekick-shared';
374
+
375
+ const { providers, diagnostics } = createSessionProviders({
376
+ onDiagnostic: (diagnostic) => {
377
+ // e.g. { providerId: 'opencode', kind: 'sqlite_missing',
378
+ // severity: 'warning', phase: 'query', message: '...' }
379
+ log(diagnostic);
380
+ },
381
+ });
382
+ ```
383
+
384
+ Repeated identical degradations are coalesced, so the diagnostic stream stays
385
+ bounded across long-lived polling and subscription cycles.
386
+
263
387
  ### Detect the active session provider
264
388
 
389
+ For a single-provider surface, auto-detect and construct just one:
390
+
265
391
  ```typescript
266
392
  import {
267
393
  detectProvider,
@@ -477,7 +603,7 @@ const parser = new JsonlParser(
477
603
  parser.processChunk(rawData);
478
604
  ```
479
605
 
480
- 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[]`.
606
+ 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[]`.
481
607
 
482
608
  ### Tail raw JSONL events incrementally
483
609
 
@@ -553,6 +679,28 @@ console.log(status.claude.present, status.claude.email);
553
679
  console.log(status.codex.present, status.codex.label);
554
680
  ```
555
681
 
682
+ To react to logins, logouts, and switches without polling that status yourself,
683
+ subscribe to `onAccountsChanged()`. It combines process-local mutation signals,
684
+ filesystem watches on the account stores, and a low-frequency catch-up poll —
685
+ and only emits when the status actually changed:
686
+
687
+ ```typescript
688
+ import { onAccountsChanged } from 'sidekick-shared';
689
+
690
+ const subscription = onAccountsChanged(
691
+ ({ reason, status }) => {
692
+ // reason: 'local' | 'filesystem' | 'poll'
693
+ render(status.claude, status.codex);
694
+ },
695
+ { emitCurrent: true },
696
+ );
697
+ // later: subscription.dispose();
698
+ ```
699
+
700
+ `QuotaPoller`, `MultiProviderQuotaService`, and `CodexQuotaWatcher` use the same
701
+ signal internally: they stay dormant while no matching account exists and wake
702
+ when one appears.
703
+
556
704
  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):
557
705
 
558
706
  ```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;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._onRawAccountsChanged = _onRawAccountsChanged;
4
+ exports._emitAccountsChanged = _emitAccountsChanged;
5
+ const listeners = new Set();
6
+ function _onRawAccountsChanged(listener) {
7
+ listeners.add(listener);
8
+ return () => listeners.delete(listener);
9
+ }
10
+ function _emitAccountsChanged(reason) {
11
+ for (const listener of listeners) {
12
+ try {
13
+ listener(reason);
14
+ }
15
+ catch {
16
+ // Account notifications are observational and cannot break mutations.
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import { type AccountChangeReason } from './accountChangeSignal';
2
+ import { type ActiveAccountStatus } from './accountStatus';
3
+ import type { Disposable } from './quotaPoller';
4
+ export interface AccountsChangedEvent {
5
+ reason: AccountChangeReason;
6
+ status: ActiveAccountStatus;
7
+ }
8
+ export interface OnAccountsChangedOptions {
9
+ /** Emit the current status immediately after subscribing. */
10
+ emitCurrent?: boolean;
11
+ /** Catch-up poll for filesystems where watch delivery is unreliable. Default: 30s. */
12
+ pollIntervalMs?: number;
13
+ }
14
+ /**
15
+ * Observe login, logout, and active-account switches. Local Sidekick mutations
16
+ * signal immediately; filesystem watchers cover external CLIs; a low-frequency
17
+ * catch-up poll protects network mounts and watcher-hostile filesystems.
18
+ */
19
+ export declare function onAccountsChanged(listener: (event: AccountsChangedEvent) => void, options?: OnAccountsChangedOptions): Disposable;
20
+ /** Internal bridge for account mutations outside the registry module. */
21
+ export declare function _notifyAccountsChanged(): void;