sidekick-shared 0.27.4 → 0.27.6

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 (64) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +92 -45
  3. package/dist/accountHealth.d.ts +14 -0
  4. package/dist/accountHealth.js +61 -14
  5. package/dist/accountLaunch.d.ts +6 -0
  6. package/dist/accountLaunch.js +61 -4
  7. package/dist/accountLease.d.ts +5 -0
  8. package/dist/accountLease.js +98 -0
  9. package/dist/accountManager.d.ts +7 -0
  10. package/dist/accountManager.js +226 -89
  11. package/dist/accountSync.d.ts +1 -5
  12. package/dist/accountSync.js +22 -6
  13. package/dist/accountSyncTypes.d.ts +9 -0
  14. package/dist/accounts.d.ts +75 -18
  15. package/dist/accounts.js +712 -189
  16. package/dist/claudeCredentialDoctor.d.ts +50 -0
  17. package/dist/claudeCredentialDoctor.js +277 -0
  18. package/dist/claudeCredentialLedger.d.ts +15 -0
  19. package/dist/claudeCredentialLedger.js +130 -0
  20. package/dist/claudeCredentials.d.ts +20 -3
  21. package/dist/claudeCredentials.js +50 -3
  22. package/dist/claudeLiveGuard.d.ts +33 -0
  23. package/dist/claudeLiveGuard.js +46 -0
  24. package/dist/claudeProfiles.d.ts +14 -0
  25. package/dist/claudeProfiles.js +22 -0
  26. package/dist/claudeRefreshLock.d.ts +15 -0
  27. package/dist/claudeRefreshLock.js +145 -0
  28. package/dist/codexAuth.d.ts +4 -0
  29. package/dist/codexAuth.js +18 -0
  30. package/dist/codexCredentialDoctor.d.ts +33 -0
  31. package/dist/codexCredentialDoctor.js +302 -0
  32. package/dist/codexCredentialStore.d.ts +29 -0
  33. package/dist/codexCredentialStore.js +213 -0
  34. package/dist/codexKeepAlive.d.ts +2 -0
  35. package/dist/codexKeepAlive.js +116 -0
  36. package/dist/codexPaths.d.ts +8 -2
  37. package/dist/codexPaths.js +51 -7
  38. package/dist/codexProfiles.d.ts +37 -31
  39. package/dist/codexProfiles.js +842 -671
  40. package/dist/credentialIO.d.ts +67 -7
  41. package/dist/credentialIO.js +211 -47
  42. package/dist/ensureDefaultAccounts.js +1 -8
  43. package/dist/index.d.ts +17 -4
  44. package/dist/index.js +36 -11
  45. package/dist/keepAlive.d.ts +2 -1
  46. package/dist/keepAlive.js +116 -29
  47. package/dist/liveCredentialGuard.d.ts +28 -0
  48. package/dist/liveCredentialGuard.js +36 -0
  49. package/dist/node.d.ts +3 -0
  50. package/dist/node.js +4 -2
  51. package/dist/processDetection.d.ts +10 -3
  52. package/dist/processDetection.js +44 -11
  53. package/dist/promptHistory.d.ts +66 -0
  54. package/dist/promptHistory.js +37 -18
  55. package/dist/schemas/accountManager.d.ts +22 -0
  56. package/dist/schemas/accountManager.js +4 -0
  57. package/dist/schemas/assistantTurn.d.ts +2 -2
  58. package/dist/sessionPromptHistory.d.ts +120 -0
  59. package/dist/sessionPromptHistory.js +278 -0
  60. package/dist/sessionPromptSignals.d.ts +56 -0
  61. package/dist/sessionPromptSignals.js +355 -0
  62. package/dist/terminalSync.d.ts +12 -2
  63. package/dist/terminalSync.js +27 -2
  64. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ All notable changes to sidekick-shared will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.27.6] - 2026-09-26
9
+
10
+ ### Fixed
11
+
12
+ - **One usable copy per Claude refresh-token chain.** Claude Code rotates the refresh token on every refresh, so a second usable copy of a login invalidates the first ("Not logged in · Please run /login").
13
+ - **Keychain writes** use Claude Code's method: `-X <hex>` through `security -i` up to 4032 bytes, argv beyond. The old `-w '<json>'` line was silently truncated at 4095 bytes once `mcpOAuth` grew. Every write is read back; a mismatch or an unverifiable read-back throws `ClaudeCredentialWriteError`. Reads and deletes pass the account name like Claude Code, and profile items carry a `sidekick-agent-hub:<dir>` comment.
14
+ - **The live account keeps no profile-home credential.** `storeClaudeProfileCredentials()` is the chokepoint: for the live account it writes only the private backup and the sidecar. `syncLiveAccountState()` removes a leftover copy (`report.claude.demoted`) and installs it first when it is strictly newer than live (`repaired`). It also refuses to fold when the live credential's fingerprint belongs to another profile.
15
+ - **Guarded live writes.** Only `installIntoLiveClaudeStore` writes the live store, after `decideLiveClaudeWrite()`: a switch requires the outgoing login captured (mandatory even with `force`), a re-login requires a strictly fresher credential of the same account, and a rollback never clobbers a store that changed. The installed blob keeps live's `mcpOAuth` and swaps `claudeAiOauth`; profile copies hold `claudeAiOauth` only.
16
+ - **Switch.** Holds Claude Code's refresh locks (`<home>/.oauth_refresh.lock` plus the legacy lock), re-checks the live fingerprint before installing, and writes `accounts/claude/switch-journal.json`. The next sync finishes or rolls back an interrupted switch from the live fingerprint (`report.claude.journal`). Sidecars record `refreshFp`, and read-only probes keep the recorded `source`.
17
+ - **`readStoredClaudeAccount()`** is read-only and returns the fresher of the profile store and the backup. `reconcileClaudeAuthState()` skips the live account.
18
+ - **Keep-alive** skips the live account, a copy with the live refresh token, `live-sync` provenance, a profile without its own copy, and a copy older than its backup. It takes a lease under the swap lock that makes a switch to that account refuse until it is done.
19
+ - **Login homes** are recorded in the ledger before the CLI runs, and `finalizeAccountLogin`, `spawnAccountLogin`, and `cleanupAbandonedClaudeLogins` (authenticated logins after 24 h) delete their Keychain item on every exit.
20
+ - **`getAccountLaunchEnv()`** points the live Claude account at the live home and restores an inactive profile from its backup (skip with `restoreBackup: false`).
21
+ - **Leftover Keychain items.** Running against a throwaway `SIDEKICK_CONFIG_DIR` with the real `HOME` no longer creates a Keychain item for the live login.
22
+ - **Profile-home shells.** When `CLAUDE_CONFIG_DIR` points at a Sidekick profile home (a shell from `sidekick accounts env` or `shell`), `syncLiveAccountState()` reports `claude.skipped: 'managed-home'` as it does for Codex, `applyClaudeProfileToLiveHome()` refuses, and `inspectClaudeCredentialStores()` reports no live duplicate, so that profile's only usable copy is never demoted.
23
+ - **Codex OAuth credential safety:** private byte-preserved backups, shared live-write guard policy, verified outgoing capture, journal recovery, and non-bypassable consumer/lease checks replace duplicate profile credentials and timestamped stashes. Live launches use the live home. Codex keep-alive uses the official app-server refresh request and requires a verified credential advance. Sign-in homes are separate and cleaned up after use. Added `inspectCodexCredentialStores`, `repairCodexCredentialStores`, `readStoredCodexAccount`, `syncCodexLiveStateAsync`, and non-persisting health inspection (`persist: false`). File storage is required, and unrelated MCP data is preserved.
24
+
25
+ ### Added
26
+
27
+ - `readClaudeCredentialStore()` (`ok` / `absent` / `corrupt` / `error`), `decideLiveClaudeWrite()`, `claudeRefreshFingerprint()`, `claudeLoginOnly()`, `mergeClaudeLoginIntoLive()`, and `ClaudeCredentialWriteError`.
28
+ - A ledger of credential homes (`accounts/claude/credential-homes.json`, `readClaudeCredentialLedger()`), plus `inspectClaudeCredentialStores()` and `repairClaudeCredentialStores({ dryRun })`: `orphan`, `live-duplicate`, `corrupt`, and `stale-entry` findings. Ownership is proven by the ledger, the comment tag, or the profile directory, never by name.
29
+ - `discardAccountLogin(provider, loginId)` and `writeAccountLauncher(name, provider, accountId)`.
30
+ - `ProviderSyncReport` gains `demoted`, `repaired`, and `journal`, and the sidecar gains `refreshFp`; the schemas are updated.
31
+
32
+ ### Changed
33
+
34
+ - `writeActiveCredentials()` throws without a config directory. `deleteStoredCredentials()` returns whether the store is gone.
35
+ - Deprecated: `writeLauncher()`, `applyActiveClaudeToLiveHome()` (now a guarded re-login that refuses another account), and `resolveActiveClaudeHome()` (the live home for the live account).
36
+ - A global test guard (`src/testing/credentialGuard.setup.ts`, also used by the CLI and extension suites) fails any test that reaches the real `security`, `claude`, or `codex` binaries or real homes.
37
+
38
+ ## [0.27.5] - 2026-09-23
39
+
40
+ ### Added
41
+
42
+ - `collectSessionPromptHistory()` (`sidekick-shared/node` and the package root, not the browser entry) returns every human prompt of each matching Claude Code or Codex session, grouped by session.
43
+ - **Consistency.** Sessions are read from the start with the scanners of `collectPromptHistory()`, so text, fail-closed scope, first-call model and usage, and ordinals are identical.
44
+ - **Selection.** `sessionIds`, `since` (file modification time only; returned sessions are whole), and `limit` (most recent sessions with prompts).
45
+ - **Session fields.** `startedAt`, `lastActivityAt`, `cwds`, `gitBranches`, `models`, and completeness (`complete`, `truncated`, `droppedPrompts`, `excludedRecords`).
46
+ - **Opt-in content.** `include.replies` adds the agent's last text for each prompt's turn (for Codex, `task_complete.last_agent_message` first). `include.signals` adds `interrupt`, `toolRejected` (Claude only, with the typed rejection reason in full), `toolError` (tool name plus error text cut to 1024 characters), `compaction`, `apiError`, and `rollback`, each with the ordinal of the prompt it followed.
47
+ - **Scope.** Replies and signals that follow an out-of-scope prompt are dropped.
48
+ - **Bounds.** `maxSessions`, `maxSessionBytes` (default 256 MiB), `maxTotalBytes` (default 256 MiB), `maxRecordBytes`, `deadlineMs`, and `signal`. A session that `maxSessions`, `maxTotalBytes`, `deadlineMs`, or `signal` interrupts, or that changes while it is read, is listed in `unread`, never returned partially; a session over `maxSessionBytes` is returned with `truncated: true`.
49
+ - New exported types: `CollectSessionPromptHistoryOptions`, `PromptHistorySession`, `PromptHistorySessionPrompt`, `PromptHistorySessionRef`, `PromptHistoryReply`, `PromptHistorySignal`, `PromptHistorySignalKind`, `SessionPromptHistoryBound`, `SessionPromptHistoryBounds`, `SessionPromptHistoryInclude`, `SessionPromptHistoryResult`, and `SessionPromptHistoryStats`.
50
+
8
51
  ## [0.27.4] - 2026-09-23
9
52
 
10
53
  ### 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
+ - **0.27.6: one usable copy per login** — Claude Code and Codex rotate refresh tokens, so a second usable copy of a login invalidates the first ("Not logged in · Please run /login"). The live account now keeps no profile-home credential, only a private backup; `syncLiveAccountState()` demotes a leftover copy (installing it first when it is newer) and reports `demoted`, `repaired`, and `journal`. Live writes pass `decideLiveClaudeWrite()` or the Codex equivalent: a switch captures the outgoing login first even with `force`, holds Claude Code's refresh lock, is journaled so the next sync finishes or rolls it back, and keeps live MCP tokens. Claude Keychain writes use Claude Code's hex method and are read back (`ClaudeCredentialWriteError`), so blobs over about 4 KB are no longer truncated. New root APIs: `readClaudeCredentialStore()`, `inspectClaudeCredentialStores()` / `repairClaudeCredentialStores()`, `inspectCodexCredentialStores()` / `repairCodexCredentialStores()`, `readClaudeCredentialLedger()`, `readStoredCodexAccount()`, `syncCodexLiveStateAsync()`, `discardAccountLogin()`, `writeAccountLauncher()`, and non-persisting health inspection (`persist: false`). `writeActiveCredentials()` now requires a config directory; `writeLauncher()`, `applyActiveClaudeToLiveHome()`, and `resolveActiveClaudeHome()` are deprecated.
13
+ - **0.27.5: whole-session prompt history** — `collectSessionPromptHistory()` (root and Node) returns every human prompt of each matching Claude Code or Codex session, grouped by session and read from the start with the `collectPromptHistory()` scanners, so text, scope, model and usage, and ordinals are identical. Select with `sessionIds`, `since` (file mtime), or `limit`; each session carries `startedAt` / `lastActivityAt`, `cwds`, `gitBranches`, `models`, and `complete` / `truncated` / `droppedPrompts` / `excludedRecords`. `include.replies` adds the agent's last text for each prompt, and `include.signals` adds `interrupt`, `toolRejected`, `toolError`, `compaction`, `apiError`, and `rollback` signals. Sessions that `maxSessions`, `maxTotalBytes`, the deadline, or an abort cut off are listed in `unread` instead of being returned partially.
12
14
  - **0.27.4: usage events from the Node entry** — `sidekick-shared/node` now exports `createSessionProviders()`, `SYNTHETIC_MODEL`, and the `ProviderId` / `SessionProviderBase` / `SessionProviderDiagnostic` / `CreateSessionProviders*` types, so `collectUsageEvents()` callers never import the package root (a CommonJS barrel that bundlers cannot tree-shake). `collectUsageEvents()` accepts an optional `signal` that stops the collection before listing and between sessions.
13
15
  - **0.27.3: honest session totals** — Claude Code split lines are counted once: `ClaudeUsageDeduper` keeps a `message.id`'s first usage, strips identical repeats, and turns streamed growth into a `usageKind: 'correction'` delta that `EventAggregator` adds to tokens and cost without counting another call. `resolveCodexCallUsage()` drops duplicate Codex `token_count` events. `collectSessionTokenTotals()` (root and Node) returns `mainThread`, per-`subagents`, `subagentTotal`, and `combined` in the `summarizeTokens()` vocabulary, and `formatTokenBreakdown()` renders one line whose parts add up to the headline. `TokenSummary` gains `reasoning` / `billedOutsideBuckets`, and `SubagentStats` from every provider now carries cache, reasoning, and total tokens. The session dumps and `HtmlReportOptions` accept `subagents`, `readSessionReportInputs()` returns them, `collectUsageEvents()` reads Claude Code subagent transcripts, and `USAGE_CACHE_VERSION` 2 re-reads caches built with the old counting.
14
16
  - **0.27.2: streamed prompt history** — `collectPromptHistory()` reads session logs of any size in bounded, resumable passes instead of skipping files over `maxFileBytes`: every call keeps what it read, the cursor (`version: 3`) continues where each file stopped, and a pending prompt keeps its answer state so a far-away answer still settles it. Adds `maxRecordBytes` and `maxEntries` bounds, `hasMore`, permanently skipped records in `exclusions`, and rewrite detection.
@@ -92,56 +94,56 @@ returns the platform default that both overrides bypass.
92
94
 
93
95
  ## API Overview
94
96
 
95
- | Module | Description |
96
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
97
- | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
98
- | **Paths** | Config directory resolution, project data paths, workspace encoding |
99
- | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
100
- | **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 |
101
- | **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. As of 0.26.5 Claude Code and Codex enumerate through a per-instance `DirectoryListingCache` (one stat per unchanged directory) and implement `statWatchedSessionFile()` so a watch event resolves with one stat; `splitWatchedPath()` and the `ListSessionFilesOptions` / `WatchedSessionFile` / `SessionEnumerationCounters` types are exported alongside |
102
- | **Observed Sessions (V1)** | Versioned provider-neutral observation contracts — `createProviderSessionAdapterV1()` / `derivePendingUserRequestV1()` with `ObservedAgentSessionV1`, `ProviderCapabilitiesV1`, `PendingUserRequestV1`, `SessionEvidenceRefV1`, and provenance/confidence wrapper types (Zod schemas in `sidekick-shared/schemas`) |
103
- | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
104
- | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
105
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), the shared token vocabulary (`summarizeTokens()`, `formatTokenBreakdown()`, `TOKEN_*_LABEL`s) and session totals incl. subagents (`collectSessionTokenTotals()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
106
- | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
107
- | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
108
- | **Analytics** | Session-quality scoring (beta) with factor contributions and weekly trends (`scoreSessionQuality()`, `calculateQualityTrend()`), code-impact metrics including cost per changed line and per-model churn/cost (`calculateCodeImpact()`), and reported-vs-heuristic compaction accounting (`calculateCompactionLedger()`, `formatCompactionLedger()`) |
109
- | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
110
- | **Session Context** | Provider-neutral context evidence snapshots (`buildSessionContextSnapshot()`, `calculateSessionContextPressure()`, `createSessionContextProjector()`, `readSessionContextSnapshot()`): layered evidence sources, low/medium/high context pressure, and observed capabilities (tools, MCP servers, permission mode, rate limits) |
111
- | **Assistant Turns** | Browser-safe timeline/process/answer projection for provider-normalized assistant turns (`segmentAssistantTurn()`, `assistantTurnEventsFromSessionEvents()`), including interleaved reasoning, compact tool groups, and Claude `Task` subagent refs without prompt leakage |
112
- | **Report** | Self-contained HTML session report generation; `readSessionReportInputs()` returns metrics, transcript, and `subagents` for the session total |
113
- | **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows) |
114
- | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams (plus Codex reset credits via `fetchCodexResetCreditsFromApi()`, attached to quota state by `fetchCodexQuotaFromApi()`), and authoritative z.ai Coding Plan quota fetching via `resolveZaiQuota()` / `fetchZaiQuotaFromApi()` (reads z.ai's `api/monitor/usage/quota/limit` endpoint with cached-snapshot fallback). The observed-traffic z.ai estimator was removed from the package root exports in 0.23.0 and every remaining export of it is `@deprecated` — `resolveZaiQuota()` is the single supported z.ai quota API |
115
- | **Provider Status** | `fetchProviderServiceStatus()` returns explicit observed/unavailable public status with components and unresolved incidents from status.claude.com and status.openai.com; `fetchProviderStatus()` / `fetchOpenAIStatus()` remain as lossy legacy projections (`toLegacyProviderStatus()`) |
116
- | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` retains the legacy Claude Code path probe; `runDoctor({ provider })` diagnoses the selected session provider |
117
- | **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`, exported only from `sidekick-shared/schemas`) — 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 |
118
- | **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 |
119
- | **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5.1 / Opus 5.5 / Opus 5 / Opus 4.8 / Opus 4.7 1M and GPT-5.x / GPT-6 variants), pricing tables with optional LiteLLM hydration, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids |
120
- | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback. Stays dormant while no account exists and wakes through the account-change signal |
121
- | **Multi-Provider Quota** | `MultiProviderQuotaService` orchestrates Claude polling + peak-hours + account labels + Codex/z.ai quota updates behind one typed event stream. `CodexQuotaWatcher` watches the active Codex rollout for live rate limits with snapshot fallback; z.ai quota uses the API resolver with snapshot fallback. All three stay dormant without a matching account and wake on account changes |
122
- | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for startup bootstrap (migrations, abandoned-login cleanup, then `syncLiveAccountState()`), `syncLiveAccountState()` to register live logins the registry has never seen under their email, fold rotated tokens into their profile, and merge duplicate Codex profiles, `getAccountHealth()` / `listAccountsWithHealth()` for secret-free `fresh`/`expiring`/`expired`/`unknown`/`missing` health with access/refresh expiry, `getActiveAccountStatus()` for a single-pass active-account read across providers, `onAccountsChanged()` for push-based login/logout/switch notification (process-local mutations, filesystem watches, and a catch-up poll), and live-first `resolveActiveClaudeAccount()` /`resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
123
- | **Account Management 2.0** | Provider-neutral acquisition + switching: `beginAccountLogin()` / `getAccountLoginStatus()` / `finalizeAccountLogin()` / `spawnAccountLogin()` drive a TTY-less, profile-isolated login that doesn't disturb the active account until finalization; `listAllAccounts()` and `switchAccount()` expose a shared switcher across Claude and Codex; as of 0.26.6 switches are two-phase and verified, return a `SwitchAccountResult` with `runningConsumers`, `needsLogin`, and an `undoToken` (`undoLastSwitch()`), refuse expired or missing credentials unless forced, and `beginAccountLogin({ existingAccountId })` re-authenticates a saved profile in place. Every status/finalize/switch entry point also has an `-Async` variant (`getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `switchAccountAsync()`, …) that keeps `codex` CLI probes off the event loop — prefer those from hosts with a UI loop. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
124
- | **Isolated launch & auto-switch** | `getAccountLaunchEnv()` builds an isolated `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment (plus `envUnset`) for running a CLI as a saved account without switching, `writeLauncher()` / `removeLauncher()` write POSIX launcher scripts, `detectRunningAccountConsumers()` / `findRunningProcesses()` find CLIs and desktop apps that hold a login (`ps` / `tasklist`), `refreshInactiveAccounts()` is an opt-in keep-alive through the official CLIs, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) moves to a healthier saved account when quota crosses a configured threshold; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
125
- | **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — switching atomically swaps the profile's backed-up credentials into the system `~/.codex/auth.json`, with rotated-token staleness protection, one-time dual-home migration, and legacy multi-home session monitoring |
126
- | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
127
- | **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 |
128
- | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
97
+ | Module | Description |
98
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
100
+ | **Paths** | Config directory resolution, project data paths, workspace encoding |
101
+ | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
102
+ | **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 |
103
+ | **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. As of 0.26.5 Claude Code and Codex enumerate through a per-instance `DirectoryListingCache` (one stat per unchanged directory) and implement `statWatchedSessionFile()` so a watch event resolves with one stat; `splitWatchedPath()` and the `ListSessionFilesOptions` / `WatchedSessionFile` / `SessionEnumerationCounters` types are exported alongside |
104
+ | **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`) |
105
+ | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
106
+ | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
107
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), the shared token vocabulary (`summarizeTokens()`, `formatTokenBreakdown()`, `TOKEN_*_LABEL`s) and session totals incl. subagents (`collectSessionTokenTotals()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
108
+ | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
109
+ | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
110
+ | **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()`) |
111
+ | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
112
+ | **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) |
113
+ | **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 |
114
+ | **Report** | Self-contained HTML session report generation; `readSessionReportInputs()` returns metrics, transcript, and `subagents` for the session total |
115
+ | **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows). `readClaudeCredentialStore()` returns `ok` / `absent` / `corrupt` / `error`; `writeActiveCredentials()` requires a config directory and reads every write back (`ClaudeCredentialWriteError`). `decideLiveClaudeWrite()`, `claudeRefreshFingerprint()`, `claudeLoginOnly()`, and `mergeClaudeLoginIntoLive()` expose the live-write policy. As of 0.27.6, `inspectClaudeCredentialStores()` / `repairClaudeCredentialStores()` and `inspectCodexCredentialStores()` / `repairCodexCredentialStores()` find and repair unused, duplicate, and unreadable stores Sidekick provably owns (repairs are dry runs unless `dryRun: false`), with Claude ownership recorded by `readClaudeCredentialLedger()` |
116
+ | **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 |
117
+ | **Provider Status** | `fetchProviderServiceStatus()` returns explicit observed/unavailable public status with components and unresolved incidents from status.claude.com and status.openai.com; `fetchProviderStatus()` / `fetchOpenAIStatus()` remain as lossy legacy projections (`toLegacyProviderStatus()`) |
118
+ | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` retains the legacy Claude Code path probe; `runDoctor({ provider })` diagnoses the selected session provider |
119
+ | **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`, exported only from `sidekick-shared/schemas`) — 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 |
120
+ | **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 |
121
+ | **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5.1 / Opus 5.5 / Opus 5 / Opus 4.8 / Opus 4.7 1M and GPT-5.x / GPT-6 variants), pricing tables with optional LiteLLM hydration, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids |
122
+ | **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 |
123
+ | **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 |
124
+ | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for startup bootstrap (migrations, abandoned-login cleanup, then `syncLiveAccountState()`), `syncLiveAccountState()` to register live logins the registry has never seen under their email, fold rotated tokens into a private backup (the live account keeps no profile-home copy; `ProviderSyncReport` reports `demoted`, `repaired`, and switch-`journal` recovery), and merge duplicate Codex profiles, `getAccountHealth()` / `listAccountsWithHealth()` for secret-free `fresh`/`expiring`/`expired`/`unknown`/`missing` health with access/refresh expiry, `getActiveAccountStatus()` for a single-pass active-account read across providers, `onAccountsChanged()` for push-based login/logout/switch notification (process-local mutations, filesystem watches, and a catch-up poll), and live-first `resolveActiveClaudeAccount()` /`resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
125
+ | **Account Management 2.0** | Provider-neutral acquisition + switching: `beginAccountLogin()` / `getAccountLoginStatus()` / `finalizeAccountLogin()` / `spawnAccountLogin()` drive a TTY-less, profile-isolated login that doesn't disturb the active account until finalization; `listAllAccounts()` and `switchAccount()` expose a shared switcher across Claude and Codex; as of 0.26.6 switches are two-phase and verified, return a `SwitchAccountResult` with `runningConsumers`, `needsLogin`, and an `undoToken` (`undoLastSwitch()`), refuse expired or missing credentials unless forced, and `beginAccountLogin(provider, label, { existingAccountId })` re-authenticates a saved profile in place. As of 0.27.6 a switch always captures the outgoing login first (even with `force`), and `discardAccountLogin()` releases an abandoned login home. 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 (the live account's home holds no credential) and startup migration from legacy flat backups |
126
+ | **Isolated launch & auto-switch** | `getAccountLaunchEnv()` builds an isolated `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment (plus `envUnset`) for running a CLI as a saved account without switching — the live account gets the live home, and an inactive profile is restored from its backup first unless `restoreBackup: false` — `writeAccountLauncher()` / `removeLauncher()` write POSIX launcher scripts that resolve the account's home when they run (`writeLauncher()` is deprecated), `detectRunningAccountConsumers()` / `findRunningProcesses()` find CLIs and desktop apps that hold a login (`ps` / `tasklist`), `refreshInactiveAccounts()` is an opt-in keep-alive through the official CLIs, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) moves to a healthier saved account when quota crosses a configured threshold; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
127
+ | **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. As of 0.27.6 every account keeps a private byte-preserved backup, the live account keeps no profile copy, switches are journaled and refuse while Codex consumers run or keep-alive holds the account (`force` bypasses neither), file-mode credential storage is required, and `readStoredCodexAccount()` / `syncCodexLiveStateAsync()` are public |
128
+ | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
129
+ | **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 |
130
+ | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
129
131
 
130
132
  ## Supported import paths
131
133
 
132
134
  `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.
133
135
 
134
- | Path | Runtime | What it exposes |
135
- | ------------------------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
137
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
138
- | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, public provider service status (`fetchProviderServiceStatus()`), observed context-window persistence, recent-session/transcript reads, human prompt history (`collectPromptHistory()`), usage events from session logs (`collectUsageEvents()` with `createSessionProviders()` and `SYNTHETIC_MODEL`), session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
139
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
140
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
141
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
142
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
143
- | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, account management, quota history, the `state.json` contract) — fs-free, no Node builtins. |
144
- | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers and writers, `formatStatusline()`, burn-rate estimates, quota freshness helpers, `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()`, and `readStateFile()` / `writeStateFile()`. |
136
+ | Path | Runtime | What it exposes |
137
+ | ------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
138
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
139
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
140
+ | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, public provider service status (`fetchProviderServiceStatus()`), observed context-window persistence, recent-session/transcript reads, human prompt history (`collectPromptHistory()`, and whole sessions with `collectSessionPromptHistory()`), usage events from session logs (`collectUsageEvents()` with `createSessionProviders()` and `SYNTHETIC_MODEL`), session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
141
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
142
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
143
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
144
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
145
+ | `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. |
146
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers and writers, `formatStatusline()`, burn-rate estimates, quota freshness helpers, `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()`, and `readStateFile()` / `writeStateFile()`. |
145
147
 
146
148
  ### Browser / webview runtimes
147
149
 
@@ -360,6 +362,51 @@ for (const exclusion of result.exclusions) console.warn('record left out', exclu
360
362
  - **Permanent exclusions are reported.** A record over `maxRecordBytes` is skipped as it streams past without being buffered, even when it is still being written, and a complete line that is not JSON is skipped. Each is counted (`stats.recordsOverSizeLimit`, `stats.recordsMalformed`) and listed in `exclusions` with its byte offset. A prompt inside such a record is lost and does not consume an ordinal. A file that shrank or whose first 4 KiB changed is treated as rewritten: it is read again from the start with fresh ordinals and counted in `stats.sessionsRewritten`.
361
363
  - **Cursor versions.** Output is `version: 3`. Cursors from 0.27.0 (`version: 1`) and 0.27.1 (`version: 2`) are accepted: each file is re-verified once, and a 0.27.1 pending prompt is replayed from its line in resumable passes, so it now settles even when its answer is far away. Files 0.27.1 skipped for size were never in its cursor and are read in full on the first call. Metadata that 0.27.1 already gave up on (a pending replay that exceeded `maxFileBytes`) was not saved and cannot be recovered, and a 0.27.0 cursor still cannot recover prompts it returned without metadata.
362
364
 
365
+ ### Collect whole sessions of prompts
366
+
367
+ `collectSessionPromptHistory()` (Node-only) returns every human prompt of each matching session, grouped by session, for consumers that classify a session as a whole. It reads each session from the start with the same scanners as `collectPromptHistory()`, so text, scope, model and usage, and ordinals are identical. The replies and interaction signals that the prompt filter leaves out are opt-in.
368
+
369
+ ```typescript
370
+ import { collectSessionPromptHistory } from 'sidekick-shared/node';
371
+
372
+ const result = await collectSessionPromptHistory({
373
+ workspacePaths: ['/Users/me/code/app'], // plus git worktree siblings
374
+ providers: ['claude-code', 'codex'], // default: both
375
+ since: new Date('2026-09-01'), // sessions whose file changed since then, returned whole
376
+ // sessionIds: ['…'], // or exact session ids
377
+ // limit: 20, // most recent sessions with prompts
378
+ include: { signals: true, replies: true }, // default: prompts only
379
+ bounds: {
380
+ maxSessions: 1000,
381
+ maxSessionBytes: 256 << 20,
382
+ maxTotalBytes: 256 << 20,
383
+ deadlineMs: 60_000,
384
+ },
385
+ });
386
+
387
+ for (const session of result.sessions) {
388
+ // { provider, sessionId, startedAt, lastActivityAt, cwds, gitBranches, models,
389
+ // prompts: [{ ordinal, timestamp, text, cwd, gitBranch?, model?, usage?, metadataStatus, reply? }],
390
+ // signals?: [{ kind, timestamp, afterOrdinal, tool?, text?, textTruncated? }],
391
+ // complete, truncated, droppedPrompts, excludedRecords }
392
+ }
393
+ if (result.unread.length > 0) {
394
+ // A bound stopped the call, or a session changed while it was read:
395
+ // pass these back as `sessionIds` to continue.
396
+ }
397
+ ```
398
+
399
+ - **Whole sessions, no cursor.** Every call reads each session from its first line and never resumes one mid-file. `since` selects files by modification time and never drops older prompts from a returned session. A long-lived consumer can use the `collectPromptHistory()` cursor to learn which sessions changed, then fetch those sessions whole by `sessionIds`.
400
+ - **Bounds.** `maxSessions`, `maxTotalBytes`, the deadline, and an abort stop the call. Any session they interrupt is listed in `unread` rather than returned partially, and `boundsHit` names the bound. A session that changed while it was being read is also listed in `unread`, with no `boundsHit` entry. A session longer than `maxSessionBytes` is returned with `truncated: true`, and `boundsHit` includes `maxSessionBytes`; keep `maxSessionBytes` at or below `maxTotalBytes`, or a session larger than `maxTotalBytes` is never read. A record over `maxRecordBytes` (default 16 MiB) is skipped and counted in `excludedRecords`.
401
+ - **Completeness.** `complete` is false when the session was truncated, when `droppedPrompts` > 0 (prompts out of scope or without a timestamp, visible as gaps in `ordinal`), or when `excludedRecords` > 0 (oversized or malformed records).
402
+ - **Replies** (`include.replies`): the agent's last text for the prompt's turn. For Claude Code this is the last assistant message with text before the next prompt, with split records joined. For Codex it is `task_complete.last_agent_message`, falling back to the last agent message.
403
+ - **Signals** (`include.signals`), in log order, each with the ordinal of the prompt it followed (`afterOrdinal`, -1 before the first):
404
+ - `interrupt`: Claude `[Request interrupted by user…]`, or Codex `turn_aborted`.
405
+ - `toolRejected`: Claude only. Carries the rejection reason the person typed, in full. A rejection without a reason and the interrupt Claude Code writes after it count as one signal.
406
+ - `toolError`: a failed tool call, with the tool name and the error text cut to 1024 characters (`textTruncated`).
407
+ - `compaction`, `apiError` (including Codex usage-limit errors), and `rollback` (Codex `thread_rolled_back`).
408
+ - **Scope fails closed.** Sessions with no prompt inside the roots are omitted. Replies and signals that follow an out-of-scope prompt are dropped.
409
+
363
410
  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.
364
411
 
365
412
  ### Locate provider-native session files
@@ -12,6 +12,12 @@ export interface AccountHealthSidecar {
12
12
  lastRefreshAt?: number;
13
13
  hasRefreshToken?: boolean;
14
14
  authMode?: 'chatgpt' | 'api-key' | 'oauth';
15
+ /**
16
+ * sha256(refreshToken)[:16] of the stored provider credential: lets sync tell
17
+ * whose chain the live store holds without keeping another token copy.
18
+ */
19
+ refreshFp?: string;
20
+ /** Provenance of the stored credential; read-only probes preserve it. */
15
21
  source: AccountHealthSource;
16
22
  }
17
23
  export interface AccountHealth {
@@ -48,6 +54,8 @@ export interface AccountHealthOptions {
48
54
  * is paid once per profile and never on a hot path that opts into `cache`.
49
55
  */
50
56
  probe?: 'cache' | 'store' | 'auto';
57
+ /** Inspection-only callers must not persist health metadata. Default true. */
58
+ persist?: boolean;
51
59
  now?: number;
52
60
  }
53
61
  export declare function getHealthSidecarPath(provider: AccountProviderId, accountId: string): string;
@@ -60,6 +68,12 @@ export declare function writeClaudeHealthSidecar(accountId: string, blob: unknow
60
68
  export declare function codexHealthSidecarFromAuth(authRaw: string | null, source: AccountHealthSource, now?: number, fallbackPath?: string): AccountHealthSidecar | null;
61
69
  /** Record the health of a Codex profile from the auth file just stored. */
62
70
  export declare function writeCodexHealthSidecar(accountId: string, authRaw: string | null, source: AccountHealthSource, now?: number): AccountHealthSidecar | null;
71
+ /**
72
+ * The credential a Claude profile would use right now: the live store for the
73
+ * live account (its profile home is empty by design), otherwise the fresher of
74
+ * the profile-home store and Sidekick's backup.
75
+ */
76
+ export declare function readEffectiveClaudeCredential(accountId: string): unknown;
63
77
  /**
64
78
  * Health of one saved account. `probe: 'cache'` never spawns anything; a
65
79
  * missing sidecar on a profile that has credentials is reported as `unknown`
@@ -39,6 +39,7 @@ exports.claudeHealthSidecarFromBlob = claudeHealthSidecarFromBlob;
39
39
  exports.writeClaudeHealthSidecar = writeClaudeHealthSidecar;
40
40
  exports.codexHealthSidecarFromAuth = codexHealthSidecarFromAuth;
41
41
  exports.writeCodexHealthSidecar = writeCodexHealthSidecar;
42
+ exports.readEffectiveClaudeCredential = readEffectiveClaudeCredential;
42
43
  exports.getAccountHealth = getAccountHealth;
43
44
  exports.listAccountsWithHealth = listAccountsWithHealth;
44
45
  exports.describeClaudeCredential = describeClaudeCredential;
@@ -55,6 +56,7 @@ const credentialIO_1 = require("./credentialIO");
55
56
  const codexAuth_1 = require("./codexAuth");
56
57
  const codexPaths_1 = require("./codexPaths");
57
58
  const accountRegistry_1 = require("./accountRegistry");
59
+ const codexCredentialStore_1 = require("./codexCredentialStore");
58
60
  const atomic_1 = require("./writers/atomic");
59
61
  /** Refresh tokens that expire within this window count as `expiring`. */
60
62
  const REFRESH_EXPIRY_WARNING_MS = 3 * 24 * 60 * 60 * 1000;
@@ -93,6 +95,7 @@ function claudeHealthSidecarFromBlob(blob, source, now = Date.now()) {
93
95
  refreshExpiryEstimated: refresh?.estimated,
94
96
  hasRefreshToken: Boolean(credentials.refreshToken),
95
97
  authMode: 'oauth',
98
+ refreshFp: (0, claudeCredentials_1.claudeRefreshFingerprint)(credentials) ?? undefined,
96
99
  source,
97
100
  };
98
101
  }
@@ -114,7 +117,8 @@ function codexHealthSidecarFromAuth(authRaw, source, now = Date.now(), fallbackP
114
117
  capturedAt: now,
115
118
  accessExpiresAt: (0, codexAuth_1.readAccessTokenExpiry)(authRaw) ?? undefined,
116
119
  lastRefreshAt,
117
- hasRefreshToken: identity.authMode === 'chatgpt',
120
+ hasRefreshToken: (0, codexAuth_1.codexRefreshFingerprint)(authRaw) !== null,
121
+ refreshFp: (0, codexAuth_1.codexRefreshFingerprint)(authRaw) ?? undefined,
118
122
  authMode: identity.authMode,
119
123
  source,
120
124
  };
@@ -185,15 +189,54 @@ function isLiveCodexAccount(profile) {
185
189
  workspaceId: profile.metadata?.workspaceId,
186
190
  });
187
191
  }
188
- function probeClaudeStore(accountId, now) {
189
- const blob = (0, credentialIO_1.readActiveCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(accountId));
190
- return blob ? writeClaudeHealthSidecar(accountId, blob, 'switch', now) : null;
192
+ function readClaudeBackupBlob(accountId) {
193
+ try {
194
+ return JSON.parse(fs.readFileSync((0, claudeProfiles_1.getClaudeCredentialBackupPath)(accountId), 'utf8'));
195
+ }
196
+ catch {
197
+ return null;
198
+ }
191
199
  }
192
- function probeCodexStore(accountId, now) {
193
- const authPath = path.join((0, codexPaths_1.getCodexProfileHome)(accountId), 'auth.json');
194
- const raw = (0, codexPaths_1.readFileOrNull)(authPath);
195
- const sidecar = codexHealthSidecarFromAuth(raw, 'switch', now, authPath);
196
- if (sidecar)
200
+ /**
201
+ * The credential a Claude profile would use right now: the live store for the
202
+ * live account (its profile home is empty by design), otherwise the fresher of
203
+ * the profile-home store and Sidekick's backup.
204
+ */
205
+ function readEffectiveClaudeCredential(accountId) {
206
+ if (isLiveClaudeAccount(accountId))
207
+ return (0, credentialIO_1.readActiveCredentials)();
208
+ const store = (0, credentialIO_1.readClaudeCredentialStore)((0, claudeProfiles_1.getClaudeProfileHome)(accountId));
209
+ const fromStore = store.status === 'ok' ? store.blob : null;
210
+ const backup = readClaudeBackupBlob(accountId);
211
+ const storeFreshness = (0, claudeCredentials_1.claudeCredentialFreshness)((0, claudeCredentials_1.parseClaudeCredentialBlob)(fromStore));
212
+ const backupFreshness = (0, claudeCredentials_1.claudeCredentialFreshness)((0, claudeCredentials_1.parseClaudeCredentialBlob)(backup));
213
+ if ((0, claudeCredentials_1.parseClaudeCredentialBlob)(backup) && backupFreshness > storeFreshness)
214
+ return backup;
215
+ return (0, claudeCredentials_1.parseClaudeCredentialBlob)(fromStore) ? fromStore : null;
216
+ }
217
+ function probeClaudeStore(accountId, now, persist = true) {
218
+ const blob = readEffectiveClaudeCredential(accountId);
219
+ if (!blob)
220
+ return null;
221
+ // A probe reads; it must not rewrite provenance (keep-alive relies on it).
222
+ const source = readHealthSidecar('claude-code', accountId)?.source ??
223
+ (isLiveClaudeAccount(accountId) ? 'live-sync' : 'migration');
224
+ return persist
225
+ ? writeClaudeHealthSidecar(accountId, blob, source, now)
226
+ : claudeHealthSidecarFromBlob(blob, source, now);
227
+ }
228
+ function probeCodexStore(accountId, now, persist = true) {
229
+ const profile = (0, accountRegistry_1.listSavedAccountProfiles)('codex').find((p) => p.id === accountId);
230
+ const live = profile !== undefined && isLiveCodexAccount(profile);
231
+ const home = (0, codexCredentialStore_1.readCodexCredentialStore)(live ? (0, codexPaths_1.getSystemCodexHome)() : (0, codexPaths_1.getCodexProfileHome)(accountId));
232
+ const backup = live ? null : (0, codexCredentialStore_1.readCodexBackup)(accountId);
233
+ let raw = home.status === 'ok' ? home.snapshot.authRaw : null;
234
+ if (backup?.status === 'ok' &&
235
+ (!raw || (0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, raw) > 0))
236
+ raw = backup.snapshot.authRaw;
237
+ const source = readHealthSidecar('codex', accountId)?.source ?? (live ? 'live-sync' : 'migration');
238
+ const sidecar = codexHealthSidecarFromAuth(raw, source, now);
239
+ if (sidecar && persist)
197
240
  writeSidecar('codex', accountId, sidecar);
198
241
  return sidecar;
199
242
  }
@@ -201,12 +244,14 @@ function hasStoredClaudeCredential(accountId) {
201
244
  const home = (0, claudeProfiles_1.getClaudeProfileHome)(accountId);
202
245
  return (fs.existsSync(path.join(home, '.credentials.json')) ||
203
246
  fs.existsSync(path.join((0, claudeProfiles_1.getClaudeProfileDir)(accountId), 'health.json')) ||
204
- fs.existsSync(path.join((0, accountRegistry_1.getAccountsDir)(), 'credentials', `${accountId}.credentials.json`)) ||
247
+ fs.existsSync((0, claudeProfiles_1.getClaudeCredentialBackupPath)(accountId)) ||
205
248
  (process.platform === 'darwin' && fs.existsSync(path.join(home, '.claude.json'))));
206
249
  }
207
250
  function hasStoredCodexCredential(accountId) {
208
251
  const home = (0, codexPaths_1.getCodexProfileHome)(accountId);
209
- return (fs.existsSync(path.join(home, 'auth.json')) ||
252
+ return (fs.existsSync((0, codexPaths_1.getCodexBackupPath)(accountId)) ||
253
+ fs.existsSync((0, codexPaths_1.getCodexBackupPath)(accountId, true)) ||
254
+ fs.existsSync(path.join(home, 'auth.json')) ||
210
255
  fs.existsSync(path.join(home, '.credentials.json')));
211
256
  }
212
257
  /**
@@ -224,17 +269,19 @@ function getAccountHealth(provider, accountId, options = {}) {
224
269
  let sidecar = null;
225
270
  if (probe === 'store') {
226
271
  sidecar =
227
- provider === 'codex' ? probeCodexStore(accountId, now) : probeClaudeStore(accountId, now);
272
+ provider === 'codex'
273
+ ? probeCodexStore(accountId, now, options.persist)
274
+ : probeClaudeStore(accountId, now, options.persist);
228
275
  }
229
276
  sidecar ??= readHealthSidecar(provider, accountId);
230
277
  if (!sidecar && provider === 'codex') {
231
278
  // Codex sidecars are cheap to derive from the plaintext file.
232
- sidecar = probeCodexStore(accountId, now);
279
+ sidecar = probeCodexStore(accountId, now, options.persist);
233
280
  }
234
281
  if (!sidecar && probe === 'auto' && hasStoredClaudeCredential(accountId)) {
235
282
  // First look at a profile that predates the health model: read its store
236
283
  // once and record the sidecar so later reads are cache hits.
237
- sidecar = probeClaudeStore(accountId, now);
284
+ sidecar = probeClaudeStore(accountId, now, options.persist);
238
285
  }
239
286
  const base = { providerId: provider, accountId, checkedAt: now, isLive };
240
287
  if (!sidecar) {