sidekick-shared 0.26.0 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to sidekick-shared will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.26.2] - 2026-09-06
9
+
10
+ ### Added
11
+
12
+ - `DoctorOptions.provider` selects the session provider to diagnose; `HealthReport.sessionProvider` records it.
13
+ - Optional watcher `onBatchComplete` callback for persisting state after all events in a read have been delivered.
14
+ - `SessionSearchOptions.projectPath` and `signal` for provider-aware workspace filtering and cancellation; legacy `projectSlug` remains supported.
15
+
16
+ ### Fixed
17
+
18
+ - JSONL watcher and provider reader checkpoints end at complete lines, retaining partial UTF-8 characters across reads and restarts. Codex restores parser context when seeking.
19
+ - Watcher failures retain polling fallback; OpenCode watchers share the canonical data-directory resolver.
20
+ - Session search enumerates through providers, includes database-only sessions, and deduplicates paths.
21
+ - Doctor checks the selected session provider, treats unrelated integrations as informational, and reads account status without changing account selection.
22
+
23
+ ## [0.26.1] - 2026-09-05
24
+
25
+ ### Fixed
26
+
27
+ - Static model metadata verified on 2026-09-05 adds exact Fable 5.1 pricing/context entries in dashed and dotted forms, with its $0.25/M cache-read rate, and GPT-6 Astra pricing with a 1.05M context window. GPT-5.6 Sol/Terra/Luna rates and the Sol alias are current; GPT-5.4 mini cache reads cost $0.075/M and mini/nano context windows are 400K. Sonnet 5's permanent $2/$10 rate is documented. Catalog overrides, observed limits, and historical models are preserved.
28
+ - `resolveQuota({ preferFresh: false })` tries the API before local samples when API access is allowed. Failed live queries select the newer session or cached sample by capture time, prefer the cache on ties, and retain the API failure. Only a selected session sample is persisted.
29
+ - Codex quota failures name Codex instead of Claude or Anthropic. Default cache preferences and API-disabled resolution are unchanged.
30
+
8
31
  ## [0.26.0] - 2026-09-05
9
32
 
10
33
  ### Added
package/README.md CHANGED
@@ -9,7 +9,8 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
- - **One token vocabulary and cost provenance (0.26.0)** — `summarizeTokens()` gives every surface the same `total` (input + output + cache writes + cache reads) and `context` (input + cache) figures; `AggregatedTokens.costUsd` carries `costProvenance` so reports can say whether a cost was provider-reported or estimated; `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` turn Claude Code's status-line JSON into an official `statusline` quota source; `readQuotaSnapshot()` reports `ageMs` / `freshness` and keeps the sample's origin as `capturedSource`; `resolveQuota()` is the one quota resolution path (fresh persisted sample → session logs → provider API → older sample) shared by the CLI, the MCP server, and both dashboards; `readSessionFileStats()` / `computeSessionFileStats()` give every provider one `readSessionStats()` over the shared aggregator, with `availability`, `costProvenance`, and `toolFailures` on `SessionFileStats`; `collectUsageEvents()` reads priced usage events from session logs with a per-session cache, `computeBillingBlocks()` groups them into five-hour billing blocks, and `bucketUsage()` turns them into day, week, month, or session rows; `applySessionSummary()` and `importSessionHistory()` let any host fold finished sessions into `historical-data.json` the way the extension does; observed sessions are read once per discovery, re-read only when their fingerprint changes, and classified from events by `classifySessionActivity()`; `readSessionReportInputs()` derives report metrics and transcript from one read; `writeStateFile()` / `readStateFile()` (also on the `statusline` subpath) maintain the public, versioned `state.json` for external tools, validated by `sidekickStateFileSchema`; `getScheduledPeakHoursState()` and `evaluateQuotaThresholds()` are shared by the CLI and extension dashboards.
12
+ - **0.26.2: reliable monitoring and search** — complete-line JSONL cursors preserve Unicode and Codex parser context; watcher `onBatchComplete` signals a delivered batch. `searchSessions()` adds workspace-path filtering and cancellation, and Doctor accepts an explicit session provider.
13
+ - **One token vocabulary and cost provenance (0.26.0)** — `summarizeTokens()` gives every surface the same `total` (input + output + cache writes + cache reads) and `context` (input + cache) figures; `AggregatedTokens.costUsd` carries `costProvenance` so reports can say whether a cost was provider-reported or estimated; `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` turn Claude Code's status-line JSON into an official `statusline` quota source; `readQuotaSnapshot()` reports `ageMs` / `freshness` and keeps the sample's origin as `capturedSource`; `resolveQuota()` shares quota resolution across CLI, MCP, and dashboard startup, with default local-sample preferences and an API-first option used by one-shot Codex queries; `readSessionFileStats()` / `computeSessionFileStats()` give every provider one `readSessionStats()` over the shared aggregator, with `availability`, `costProvenance`, and `toolFailures` on `SessionFileStats`; `collectUsageEvents()` reads priced usage events from session logs with a per-session cache, `computeBillingBlocks()` groups them into five-hour billing blocks, and `bucketUsage()` turns them into day, week, month, or session rows; `applySessionSummary()` and `importSessionHistory()` let any host fold finished sessions into `historical-data.json` the way the extension does; observed sessions are read once per discovery, re-read only when their fingerprint changes, and classified from events by `classifySessionActivity()`; `readSessionReportInputs()` derives report metrics and transcript from one read; `writeStateFile()` / `readStateFile()` (also on the `statusline` subpath) maintain the public, versioned `state.json` for external tools, validated by `sidekickStateFileSchema`; `getScheduledPeakHoursState()` and `evaluateQuotaThresholds()` are shared by the CLI and extension dashboards.
13
14
  - **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.
14
15
  - **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).
15
16
  - **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.
@@ -104,7 +105,7 @@ returns the platform default that both overrides bypass.
104
105
  | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
105
106
  | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams (plus Codex reset credits via `fetchCodexResetCreditsFromApi()`, attached to quota state by `fetchCodexQuotaFromApi()`), and authoritative z.ai Coding Plan quota fetching via `resolveZaiQuota()` / `fetchZaiQuotaFromApi()` (reads z.ai's `api/monitor/usage/quota/limit` endpoint with cached-snapshot fallback). The observed-traffic z.ai estimator was removed from the package root exports in 0.23.0 and every remaining export of it is `@deprecated` — `resolveZaiQuota()` is the single supported z.ai quota API |
106
107
  | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
107
- | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` exposes the per-provider session probe |
108
+ | **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 |
108
109
  | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, quota, account status, account management, quota history, and the public `state.json` contract (`sidekickStateFileSchema`) — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Account and quota APIs guarantee results valid against the same-release schemas. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
109
110
  | **Extractors** | Pure functions for single-event processing: `extractTokenUsage()`, `extractToolCall()` (top-level `tool_use`), `extractToolCalls()` (assistant content blocks), plus Node-only session asset extraction (`gatherAssetsForCwd()`, `readClaudeAssets()`, `readCodexAssets()`) for URLs, file paths, commands, and plans |
110
111
  | **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 |
@@ -475,24 +476,25 @@ if (!peak.unavailable && peak.isPeak) {
475
476
  }
476
477
  ```
477
478
 
478
- ### Resolve quota with one precedence
479
+ ### Resolve quota with shared source policies
479
480
 
480
481
  ```typescript
481
482
  import { resolveQuota } from 'sidekick-shared';
482
483
 
483
- // Fresh persisted sample (status line, session, or API; under five minutes) →
484
+ // Default: fresh persisted sample (status line, session, or API; under five minutes) →
484
485
  // session logs (Codex rollouts) → provider API → older sample labelled by age.
485
486
  const quota = await resolveQuota({ providerId: 'claude-code' });
486
487
  console.log(quota.resolution); // 'snapshot-fresh' | 'session' | 'api' | 'snapshot-aging' | 'snapshot-stale' | 'unavailable'
487
488
  console.log(quota.source, quota.capturedSource, quota.freshness, quota.ageMs);
488
489
 
489
- // `--refresh` semantics: ask the API first.
490
+ // Ask the API before any local sample: used by every one-shot Codex query
491
+ // and by --refresh for other providers. API failures retain local fallbacks.
490
492
  await resolveQuota({ providerId: 'codex', workspacePath: process.cwd(), preferFresh: false });
491
493
  // Paint from local samples only (a poller will fetch live data next).
492
494
  await resolveQuota({ providerId: 'claude-code', allowApi: false });
493
495
  ```
494
496
 
495
- `resolveQuota()` is what `sidekick quota`, `quota --all`, the MCP `get_quota_status` tool, and both dashboards call, so every surface reports the same numbers and the same `resolution`. Every side effect is injectable (`readSnapshot`, `writeSnapshot`, `fetchImpl`, `getClaudeAccessToken`, the account resolvers) for tests.
497
+ `resolveQuota()` is shared by `sidekick quota`, `quota --all`, the MCP `get_quota_status` tool, and dashboard startup. CLI and MCP Codex queries pass `preferFresh: false` to fetch live utilization and reset credits every time; dashboard schedules and default cache preferences are unchanged. After a failed live query, the newer session or persisted sample wins by `capturedAt`, with ties going to the persisted sample and missing timestamps treated as oldest. Only a selected session sample is persisted. Fallback results keep their source and age plus the API `failure` descriptor. `allowApi: false` always prevents network access, including when `preferFresh` is false. Every side effect is injectable (`readSnapshot`, `writeSnapshot`, `fetchImpl`, `getClaudeAccessToken`, the account resolvers) for tests.
496
498
 
497
499
  ### Fetch subscription quota
498
500
 
@@ -669,6 +671,26 @@ const tail = createJsonlTail({
669
671
  tail.start();
670
672
  ```
671
673
 
674
+ `SessionWatcherCallbacks.onBatchComplete` is also available through `createWatcher()`. Save aggregation state only after a complete event batch and only when the state includes the full history (a completed replay or a compatible restored checkpoint). Live-only state is incomplete and must not replace a full-history checkpoint. JSONL positions end at complete lines; Codex readers reconstruct parser context when seeking. OpenCode's SQLite timestamp cursor does not persist row-version deduplication, so the dashboards replay OpenCode history instead of restoring a snapshot.
675
+
676
+ ### Search a workspace and diagnose its provider
677
+
678
+ ```typescript
679
+ import { CodexProvider, searchSessions, runDoctor } from 'sidekick-shared';
680
+
681
+ const provider = new CodexProvider();
682
+ const controller = new AbortController();
683
+ const matches = await searchSessions(provider, 'authentication', {
684
+ projectPath: '/path/to/project',
685
+ signal: controller.signal,
686
+ maxResults: 20,
687
+ });
688
+ const report = await runDoctor({ cwd: '/path/to/project', provider: 'codex' });
689
+ console.log(matches, report.sessionProvider, report.checks);
690
+ ```
691
+
692
+ `projectPath` resolves the workspace canonically and takes precedence over the legacy `projectSlug`. Cancellation is checked between session reads; it does not interrupt an in-progress synchronous provider read. `HealthReport.sessionProvider` is optional for compatibility. `getSessionDiagnostics()` remains a Claude Code path diagnostic; use `runDoctor()` for provider-aware checks.
693
+
672
694
  ### Poll quota with backoff
673
695
 
674
696
  ```typescript
package/dist/doctor.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type ActiveAccountStatus } from './accountStatus';
2
2
  import { type ProjectIdentity } from './paths';
3
3
  import { type ProviderStatusState } from './providerStatus';
4
+ import type { ProviderId } from './providers/types';
4
5
  import { type OpenCodeDbRuntimeStatus } from './providers/openCodeDatabase';
5
6
  export type HealthCheckStatus = 'ok' | 'warning' | 'error' | 'info';
6
7
  export interface HealthCheck {
@@ -38,10 +39,14 @@ export interface HealthReport {
38
39
  claude: ProviderStatusState;
39
40
  openai: ProviderStatusState;
40
41
  };
42
+ /** Legacy Claude Code path diagnostics, retained for compatibility. */
41
43
  sessions: SessionDiagnostics;
44
+ sessionProvider?: ProviderId;
42
45
  }
43
46
  export interface DoctorOptions {
44
47
  cwd?: string;
48
+ /** Session provider to diagnose; defaults to auto-detection. */
49
+ provider?: ProviderId | 'auto';
45
50
  deprecatedSettings?: DeprecatedSetting[];
46
51
  openCodeStatus?: OpenCodeDbRuntimeStatus;
47
52
  fetchStatuses?: () => Promise<{
package/dist/doctor.js CHANGED
@@ -44,6 +44,8 @@ const paths_1 = require("./paths");
44
44
  const sessionPathResolver_1 = require("./parsers/sessionPathResolver");
45
45
  const providerStatus_1 = require("./providerStatus");
46
46
  const openCode_1 = require("./providers/openCode");
47
+ const factory_1 = require("./providers/factory");
48
+ const detect_1 = require("./providers/detect");
47
49
  const openCodeDatabase_1 = require("./providers/openCodeDatabase");
48
50
  /** Collects the Claude Code path-resolution detail used by both host surfaces. */
49
51
  function getSessionDiagnostics(workspacePath) {
@@ -100,15 +102,66 @@ function statusUnavailable() {
100
102
  updatedAt: new Date().toISOString(),
101
103
  };
102
104
  }
105
+ async function checkSessionProvider(providerId, workspacePath) {
106
+ const { providers } = (0, factory_1.createSessionProviders)({ providerIds: [providerId] });
107
+ const provider = providers[0];
108
+ const title = `Session discovery (${provider?.displayName ?? providerId})`;
109
+ try {
110
+ if (!provider)
111
+ throw new Error(`${providerId} could not be initialized.`);
112
+ const sessions = provider.listSessionFilesAsync
113
+ ? await provider.listSessionFilesAsync(workspacePath)
114
+ : provider.findAllSessions(workspacePath);
115
+ if (sessions.length > 0) {
116
+ return {
117
+ id: 'session_discovery',
118
+ status: 'ok',
119
+ title,
120
+ message: `Found ${sessions.length} ${provider.displayName} session${sessions.length === 1 ? '' : 's'} for ${workspacePath}.`,
121
+ };
122
+ }
123
+ const operation = provider.getLastOperationStatus?.();
124
+ const runtime = provider.getRuntimeStatus?.();
125
+ const issue = operation && !operation.usable
126
+ ? operation.runtimeStatus
127
+ : runtime && !runtime.available
128
+ ? runtime
129
+ : undefined;
130
+ return {
131
+ id: 'session_discovery',
132
+ status: 'warning',
133
+ title,
134
+ message: issue?.message ?? `No ${provider.displayName} sessions found for ${workspacePath}.`,
135
+ repair: issue?.kind === 'sqlite_missing'
136
+ ? 'Install sqlite3 and ensure it is on PATH, then rerun sidekick doctor.'
137
+ : `Start ${provider.displayName} in this project, or inspect ${provider.getSessionDirectory(workspacePath)}.`,
138
+ };
139
+ }
140
+ catch (error) {
141
+ return {
142
+ id: 'session_discovery',
143
+ status: 'warning',
144
+ title,
145
+ message: error instanceof Error ? error.message : String(error),
146
+ repair: `Check ${providerId} session storage permissions and rerun sidekick doctor.`,
147
+ };
148
+ }
149
+ finally {
150
+ for (const candidate of providers)
151
+ candidate.dispose();
152
+ }
153
+ }
103
154
  async function runDoctor(options = {}) {
104
155
  const project = (0, paths_1.resolveProjectIdentity)(options.cwd);
105
156
  const sessions = getSessionDiagnostics(project.cwd);
106
- const accounts = (0, accountStatus_1.getActiveAccountStatus)();
157
+ const accounts = (0, accountStatus_1.getActiveAccountStatus)(undefined, { selfHeal: false });
158
+ const sessionProvider = options.provider && options.provider !== 'auto' ? options.provider : (0, detect_1.detectProvider)();
107
159
  let openCode = options.openCodeStatus;
108
160
  if (!openCode) {
109
161
  const database = new openCodeDatabase_1.OpenCodeDatabase((0, openCode_1.getOpenCodeDataDir)());
110
162
  database.open();
111
163
  openCode = database.getRuntimeStatus();
164
+ database.close();
112
165
  }
113
166
  const providerStatus = await (options.fetchStatuses ??
114
167
  (async () => {
@@ -130,20 +183,7 @@ async function runDoctor(options = {}) {
130
183
  title: 'Project identity',
131
184
  message: `Canonical slug: ${project.canonicalSlug}`,
132
185
  });
133
- checks.push(sessions.discoveredSessionDir
134
- ? {
135
- id: 'session_discovery',
136
- status: 'ok',
137
- title: 'Session discovery',
138
- message: `Found Claude Code sessions at ${sessions.discoveredSessionDir}.`,
139
- }
140
- : {
141
- id: 'session_discovery',
142
- status: 'warning',
143
- title: 'Session discovery',
144
- message: `No Claude Code session directory found for ${project.cwd}.`,
145
- repair: `Start Claude Code in this project, or inspect ${sessions.expectedSessionDir}.`,
146
- });
186
+ checks.push(await checkSessionProvider(sessionProvider, project.resolvedCwd));
147
187
  const openCodeCheck = openCode.available
148
188
  ? {
149
189
  id: 'opencode_sqlite',
@@ -167,16 +207,22 @@ async function runDoctor(options = {}) {
167
207
  ? 'Install sqlite3 and ensure the sqlite3 executable is on PATH, then rerun sidekick doctor.'
168
208
  : 'Make sqlite3 executable and verify the OpenCode database is readable.',
169
209
  };
210
+ if (sessionProvider !== 'opencode' && openCodeCheck.status === 'warning') {
211
+ openCodeCheck.status = 'info';
212
+ openCodeCheck.message += ' This does not affect the selected session provider.';
213
+ }
170
214
  checks.push(openCodeCheck);
171
215
  checks.push({
172
216
  id: 'accounts',
173
- status: accounts.ok ? 'ok' : 'warning',
217
+ status: accounts.ok ? 'ok' : 'info',
174
218
  title: 'Accounts and credentials',
175
219
  message: accounts.ok
176
220
  ? `Detected ${[accounts.claude.present && 'Claude', accounts.codex.present && 'Codex'].filter(Boolean).join(' and ')} credentials.`
177
221
  : (accounts.error ?? 'No Claude or Codex credentials were detected.'),
178
222
  ...(!accounts.ok
179
- ? { repair: 'Sign in with claude or codex, then rerun sidekick doctor.' }
223
+ ? {
224
+ repair: 'Sign in with claude or codex to refresh account quota. Existing sessions can be monitored without saved credentials.',
225
+ }
180
226
  : {}),
181
227
  });
182
228
  const unavailableProviders = Object.entries(providerStatus)
@@ -187,7 +233,11 @@ async function runDoctor(options = {}) {
187
233
  .map(([name]) => name);
188
234
  checks.push({
189
235
  id: 'provider_api',
190
- status: degradedProviders.length > 0 ? 'warning' : unavailableProviders.length > 0 ? 'info' : 'ok',
236
+ status: degradedProviders.includes(sessionProvider === 'codex' ? 'openai' : sessionProvider === 'claude-code' ? 'claude' : '')
237
+ ? 'warning'
238
+ : degradedProviders.length > 0 || unavailableProviders.length > 0
239
+ ? 'info'
240
+ : 'ok',
191
241
  title: 'Provider API status',
192
242
  message: degradedProviders.length > 0
193
243
  ? `Provider incidents reported for: ${degradedProviders.join(', ')}.`
@@ -226,6 +276,7 @@ async function runDoctor(options = {}) {
226
276
  openCode,
227
277
  providerStatus,
228
278
  sessions,
279
+ sessionProvider,
229
280
  };
230
281
  }
231
282
  function formatHealthReport(report) {
package/dist/index.d.ts CHANGED
@@ -97,7 +97,7 @@ export type { DebugLogEntry, DebugLogFile, DebugLogLevel } from './parsers/debug
97
97
  export { OpenCodeDatabase } from './providers/openCodeDatabase';
98
98
  export { CodexDatabase } from './providers/codexDatabase';
99
99
  export { searchSessions } from './search/sessionSearch';
100
- export type { SearchResult } from './search/sessionSearch';
100
+ export type { SearchResult, SessionSearchOptions } from './search/sessionSearch';
101
101
  export { FilterEngine } from './search/advancedFilter';
102
102
  export type { FilterMode, FilterState, HighlightFormat as FilterHighlightFormat, } from './search/advancedFilter';
103
103
  export { composeContext } from './context/composer';
@@ -42,9 +42,11 @@ exports._getModelContextIds = _getModelContextIds;
42
42
  exports.resolveModelContextWindow = resolveModelContextWindow;
43
43
  exports.getModelContextWindowSize = getModelContextWindowSize;
44
44
  const modelAliases_1 = require("./modelAliases");
45
- /** Known model context window sizes (in tokens). */
45
+ /** Known model context window sizes (in tokens); current models verified 2026-09-05. */
46
46
  const MODEL_CONTEXT_SIZES = {
47
- // Claude — native 1M context (Fable 5, Opus 4.6+, Sonnet 4.6+)
47
+ // Claude — native 1M context (Fable 5+, Opus 4.6+, Sonnet 4.6+)
48
+ 'claude-fable-5-1': 1_000_000,
49
+ 'claude-fable-5.1': 1_000_000,
48
50
  'claude-fable-5': 1_000_000,
49
51
  'claude-opus-5': 1_000_000,
50
52
  'claude-opus-4-8': 1_000_000,
@@ -67,16 +69,19 @@ const MODEL_CONTEXT_SIZES = {
67
69
  'claude-3-haiku': 200_000,
68
70
  // OpenAI GPT-4.1 series (1M context)
69
71
  'gpt-4.1': 1_048_576,
70
- // OpenAI GPT-5 series (keys sorted longest-first below; explicit entries
72
+ // OpenAI GPT-5/6 series (keys sorted longest-first below; explicit entries
71
73
  // for every variant so prefix matching can't misclassify a new one).
72
74
  // These are published model maxima. Codex reports a smaller *effective*
73
75
  // window per account tier via token_count; that value wins through the
74
76
  // observed-override layer.
77
+ 'gpt-6-astra': 1_050_000,
75
78
  'gpt-5.6-sol': 1_050_000,
76
79
  'gpt-5.6-terra': 1_050_000,
77
80
  'gpt-5.6-luna': 1_050_000,
78
81
  'gpt-5.6': 1_050_000,
79
82
  'gpt-5.5': 1_050_000,
83
+ 'gpt-5.4-mini': 400_000,
84
+ 'gpt-5.4-nano': 400_000,
80
85
  'gpt-5.4': 1_050_000,
81
86
  'gpt-5.3-codex-spark': 128_000,
82
87
  'gpt-5.3-codex': 400_000,
package/dist/modelInfo.js CHANGED
@@ -50,9 +50,10 @@ Object.defineProperty(exports, "MODEL_PROVIDER_IDS", { enumerable: true, get: fu
50
50
  * against `claude-sonnet-4.5`.
51
51
  *
52
52
  * Sources:
53
- * - Anthropic: https://www.anthropic.com/pricing
54
- * - OpenAI: https://openai.com/api/pricing/
55
- * Snapshot taken: 2026-06-09. Runtime LiteLLM hydration refreshes this.
53
+ * - Anthropic: https://platform.claude.com/docs/en/about-claude/pricing
54
+ * - OpenAI: https://developers.openai.com/api/docs/models
55
+ * Current Anthropic and GPT-5.6/6 rates verified: 2026-09-05.
56
+ * Runtime LiteLLM hydration refreshes this offline baseline.
56
57
  *
57
58
  * Anthropic keys appear in both dashed (`claude-opus-4-8`, the real model-ID
58
59
  * form) and dotted (`claude-opus-4.8`, the LiteLLM catalog form) spellings —
@@ -60,6 +61,19 @@ Object.defineProperty(exports, "MODEL_PROVIDER_IDS", { enumerable: true, get: fu
60
61
  */
61
62
  const PRICING_TABLE = {
62
63
  // ── Anthropic: Claude ──
64
+ // Fable 5.1 cache reads cost 0.025x input; Fable 5 retains its 0.1x rate.
65
+ 'claude-fable-5-1': {
66
+ inputCostPerMillion: 10.0,
67
+ outputCostPerMillion: 50.0,
68
+ cacheWriteCostPerMillion: 12.5,
69
+ cacheReadCostPerMillion: 0.25,
70
+ },
71
+ 'claude-fable-5.1': {
72
+ inputCostPerMillion: 10.0,
73
+ outputCostPerMillion: 50.0,
74
+ cacheWriteCostPerMillion: 12.5,
75
+ cacheReadCostPerMillion: 0.25,
76
+ },
63
77
  'claude-fable-5': {
64
78
  inputCostPerMillion: 10.0,
65
79
  outputCostPerMillion: 50.0,
@@ -84,11 +98,8 @@ const PRICING_TABLE = {
84
98
  cacheWriteCostPerMillion: 1.0,
85
99
  cacheReadCostPerMillion: 0.08,
86
100
  },
87
- // Sonnet 5 carries the standard rate here. The introductory $2/$10 (through
88
- // 2026-08-31) arrives via catalog hydration, which supersedes this baseline.
89
- // Sonnet 5 is $2/$10, not the $3/$15 every earlier Sonnet charged, and it
90
- // carries no >200K surcharge the way Sonnet 4.5 does. The catalog agrees
91
- // across all eleven of its Sonnet 5 entries.
101
+ // Sonnet 5's $2/$10 introductory rate is now permanent. Standard pricing
102
+ // applies across its full 1M context window.
92
103
  'claude-sonnet-5': {
93
104
  inputCostPerMillion: 2.0,
94
105
  outputCostPerMillion: 10.0,
@@ -234,37 +245,44 @@ const PRICING_TABLE = {
234
245
  cacheWriteCostPerMillion: 0,
235
246
  cacheReadCostPerMillion: 0,
236
247
  },
237
- // ── OpenAI: GPT-5 family ──
238
- // Published rates, verified against the LiteLLM catalog. Runtime hydration
248
+ // ── OpenAI: GPT-5/6 families ──
249
+ // Published rates, cross-checked with the LiteLLM catalog. Runtime hydration
239
250
  // still supersedes these; they are the offline baseline.
240
251
  //
241
252
  // Every tier that ships at a rate of its own needs an entry of its own. Keys
242
253
  // are matched longest-first, so a `-mini`/`-nano`/`-pro` variant with no entry
243
254
  // silently inherits its base model's rate and reports a wrong number rather
244
255
  // than none — `gpt-5-nano` read 25x its true cost this way.
256
+ 'gpt-6-astra': {
257
+ inputCostPerMillion: 10.0,
258
+ outputCostPerMillion: 50.0,
259
+ cacheWriteCostPerMillion: 12.5,
260
+ cacheReadCostPerMillion: 1.0,
261
+ },
245
262
  'gpt-5.6-sol': {
246
- inputCostPerMillion: 5.0,
247
- outputCostPerMillion: 30.0,
248
- cacheWriteCostPerMillion: 6.25,
249
- cacheReadCostPerMillion: 0.5,
263
+ inputCostPerMillion: 4.0,
264
+ outputCostPerMillion: 20.0,
265
+ cacheWriteCostPerMillion: 5.0,
266
+ cacheReadCostPerMillion: 0.4,
250
267
  },
251
268
  'gpt-5.6-terra': {
252
- inputCostPerMillion: 2.5,
253
- outputCostPerMillion: 15.0,
254
- cacheWriteCostPerMillion: 3.13,
255
- cacheReadCostPerMillion: 0.25,
269
+ inputCostPerMillion: 2.0,
270
+ outputCostPerMillion: 12.0,
271
+ cacheWriteCostPerMillion: 2.5,
272
+ cacheReadCostPerMillion: 0.2,
256
273
  },
257
274
  'gpt-5.6-luna': {
258
- inputCostPerMillion: 1.0,
259
- outputCostPerMillion: 6.0,
260
- cacheWriteCostPerMillion: 1.25,
261
- cacheReadCostPerMillion: 0.1,
275
+ inputCostPerMillion: 0.2,
276
+ outputCostPerMillion: 1.2,
277
+ cacheWriteCostPerMillion: 0.25,
278
+ cacheReadCostPerMillion: 0.02,
262
279
  },
280
+ // The GPT-5.6 alias routes to Sol.
263
281
  'gpt-5.6': {
264
- inputCostPerMillion: 5.0,
265
- outputCostPerMillion: 30.0,
266
- cacheWriteCostPerMillion: 6.25,
267
- cacheReadCostPerMillion: 0.5,
282
+ inputCostPerMillion: 4.0,
283
+ outputCostPerMillion: 20.0,
284
+ cacheWriteCostPerMillion: 5.0,
285
+ cacheReadCostPerMillion: 0.4,
268
286
  },
269
287
  'gpt-5.5-pro': {
270
288
  inputCostPerMillion: 30.0,
@@ -282,7 +300,7 @@ const PRICING_TABLE = {
282
300
  inputCostPerMillion: 0.75,
283
301
  outputCostPerMillion: 4.5,
284
302
  cacheWriteCostPerMillion: 0,
285
- cacheReadCostPerMillion: 0.07,
303
+ cacheReadCostPerMillion: 0.075,
286
304
  },
287
305
  'gpt-5.4-nano': {
288
306
  inputCostPerMillion: 0.2,
@@ -136,6 +136,8 @@ class ClaudeCodeReader {
136
136
  sessionPath;
137
137
  parser;
138
138
  filePosition = 0;
139
+ /** Cursor after the last complete line, safe to persist across restarts. */
140
+ committedPosition = 0;
139
141
  events = [];
140
142
  _wasTruncated = false;
141
143
  decoder = new string_decoder_1.StringDecoder('utf8');
@@ -165,6 +167,7 @@ class ClaudeCodeReader {
165
167
  if (currentSize < this.filePosition) {
166
168
  this._wasTruncated = true;
167
169
  this.filePosition = 0;
170
+ this.committedPosition = 0;
168
171
  this.parser.reset();
169
172
  this.decoder = new string_decoder_1.StringDecoder('utf8');
170
173
  }
@@ -183,7 +186,11 @@ class ClaudeCodeReader {
183
186
  finally {
184
187
  fs.closeSync(fd);
185
188
  }
186
- const chunk = this.decoder.write(buffer.subarray(0, bytesRead));
189
+ const bytes = buffer.subarray(0, bytesRead);
190
+ const newline = bytes.lastIndexOf(0x0a);
191
+ if (newline >= 0)
192
+ this.committedPosition = this.filePosition + newline + 1;
193
+ const chunk = this.decoder.write(bytes);
187
194
  this.parser.processChunk(chunk);
188
195
  this.filePosition += bytesRead;
189
196
  }
@@ -198,6 +205,7 @@ class ClaudeCodeReader {
198
205
  }
199
206
  reset() {
200
207
  this.filePosition = 0;
208
+ this.committedPosition = 0;
201
209
  this.parser.reset();
202
210
  this.decoder = new string_decoder_1.StringDecoder('utf8');
203
211
  this._wasTruncated = false;
@@ -213,10 +221,11 @@ class ClaudeCodeReader {
213
221
  this.decoder = new string_decoder_1.StringDecoder('utf8');
214
222
  }
215
223
  getPosition() {
216
- return this.filePosition;
224
+ return this.committedPosition;
217
225
  }
218
226
  seekTo(position) {
219
227
  this.filePosition = position;
228
+ this.committedPosition = position;
220
229
  this.parser.reset();
221
230
  this.decoder = new string_decoder_1.StringDecoder('utf8');
222
231
  }
@@ -473,6 +473,8 @@ class CodexReader {
473
473
  onTokenUsage;
474
474
  parser;
475
475
  filePosition = 0;
476
+ /** Cursor after the last complete line, safe to persist across restarts. */
477
+ committedPosition = 0;
476
478
  lineBuffer = '';
477
479
  _wasTruncated = false;
478
480
  decoder = new string_decoder_1.StringDecoder('utf8');
@@ -487,18 +489,19 @@ class CodexReader {
487
489
  getSessionMeta() {
488
490
  return this.parser.getSessionMeta();
489
491
  }
490
- readNew() {
492
+ readNew(endPosition) {
491
493
  this._wasTruncated = false;
492
494
  const events = [];
493
495
  try {
494
496
  if (!fs.existsSync(this.rolloutPath))
495
497
  return [];
496
498
  const stats = fs.statSync(this.rolloutPath);
497
- const currentSize = stats.size;
499
+ const currentSize = Math.min(stats.size, endPosition ?? stats.size);
498
500
  // Handle truncation
499
501
  if (currentSize < this.filePosition) {
500
502
  this._wasTruncated = true;
501
503
  this.filePosition = 0;
504
+ this.committedPosition = 0;
502
505
  this.lineBuffer = '';
503
506
  this.parser.reset();
504
507
  this.decoder = new string_decoder_1.StringDecoder('utf8');
@@ -517,7 +520,11 @@ class CodexReader {
517
520
  finally {
518
521
  fs.closeSync(fd);
519
522
  }
520
- const chunk = this.decoder.write(buffer.subarray(0, bytesRead));
523
+ const bytes = buffer.subarray(0, bytesRead);
524
+ const newline = bytes.lastIndexOf(0x0a);
525
+ if (newline >= 0)
526
+ this.committedPosition = this.filePosition + newline + 1;
527
+ const chunk = this.decoder.write(bytes);
521
528
  this.filePosition += bytesRead;
522
529
  // Process lines
523
530
  const text = this.lineBuffer + chunk;
@@ -583,6 +590,7 @@ class CodexReader {
583
590
  }
584
591
  reset() {
585
592
  this.filePosition = 0;
593
+ this.committedPosition = 0;
586
594
  this.lineBuffer = '';
587
595
  this.parser.reset();
588
596
  this.decoder = new string_decoder_1.StringDecoder('utf8');
@@ -606,12 +614,14 @@ class CodexReader {
606
614
  this.decoder = new string_decoder_1.StringDecoder('utf8');
607
615
  }
608
616
  getPosition() {
609
- return this.filePosition;
617
+ return this.committedPosition;
610
618
  }
611
619
  seekTo(position) {
612
- this.filePosition = position;
613
- this.lineBuffer = '';
614
- this.decoder = new string_decoder_1.StringDecoder('utf8');
620
+ // Rebuild model, turn, and cumulative-token context before resuming.
621
+ // The prefix is parsed only; its events are already in the consumer snapshot.
622
+ this.reset();
623
+ if (position > 0)
624
+ this.readNew(position);
615
625
  }
616
626
  wasTruncated() {
617
627
  return this._wasTruncated;
@@ -23,8 +23,20 @@ function formatRetryAfter(ms) {
23
23
  function describeQuotaFailure(quota) {
24
24
  if (!quota || quota.available || !quota.failureKind)
25
25
  return null;
26
+ const isCodex = quota.providerId === 'codex';
27
+ const apiName = isCodex ? 'Codex' : 'Anthropic';
26
28
  switch (quota.failureKind) {
27
29
  case 'auth':
30
+ if (isCodex) {
31
+ return {
32
+ severity: 'error',
33
+ title: quota.httpStatus ? 'Codex sign-in expired' : 'Codex sign-in required',
34
+ message: quota.error ?? 'The current Codex login could not be used.',
35
+ detail: 'Run `codex login` to sign in, then retry quota refresh.',
36
+ alertKey: `auth:${quota.httpStatus ?? 'no-credentials'}`,
37
+ isRetryable: false,
38
+ };
39
+ }
28
40
  if (quota.error === 'No OAuth token available') {
29
41
  return {
30
42
  severity: 'error',
@@ -47,7 +59,7 @@ function describeQuotaFailure(quota) {
47
59
  return {
48
60
  severity: 'warning',
49
61
  title: 'Quota API unreachable',
50
- message: 'Could not reach Anthropic from the current environment.',
62
+ message: `Could not reach ${apiName} from the current environment.`,
51
63
  detail: 'Check connectivity, proxy, or firewall settings, then retry.',
52
64
  alertKey: 'network',
53
65
  isRetryable: true,
@@ -59,7 +71,7 @@ function describeQuotaFailure(quota) {
59
71
  message: quota.retryAfterMs != null
60
72
  ? `Retry in ${formatRetryAfter(quota.retryAfterMs)}.`
61
73
  : 'Retry shortly.',
62
- detail: quota.httpStatus != null ? `Anthropic returned HTTP ${quota.httpStatus}.` : undefined,
74
+ detail: quota.httpStatus != null ? `${apiName} returned HTTP ${quota.httpStatus}.` : undefined,
63
75
  alertKey: `rate_limit:${quota.httpStatus ?? 429}`,
64
76
  isRetryable: true,
65
77
  };
@@ -68,8 +80,8 @@ function describeQuotaFailure(quota) {
68
80
  severity: 'warning',
69
81
  title: 'Quota API unavailable',
70
82
  message: quota.httpStatus != null
71
- ? `Anthropic returned HTTP ${quota.httpStatus}. Try again shortly.`
72
- : 'Anthropic quota data is temporarily unavailable.',
83
+ ? `${apiName} returned HTTP ${quota.httpStatus}. Try again shortly.`
84
+ : `${apiName} quota data is temporarily unavailable.`,
73
85
  alertKey: `server:${quota.httpStatus ?? 'unknown'}`,
74
86
  isRetryable: true,
75
87
  };
@@ -78,7 +90,7 @@ function describeQuotaFailure(quota) {
78
90
  severity: 'error',
79
91
  title: 'Unexpected quota response',
80
92
  message: quota.httpStatus != null
81
- ? `Anthropic returned HTTP ${quota.httpStatus}.`
93
+ ? `${apiName} returned HTTP ${quota.httpStatus}.`
82
94
  : (quota.error ?? 'Quota data could not be retrieved.'),
83
95
  detail: 'This failure is not classified as retryable.',
84
96
  alertKey: `unknown:${quota.httpStatus ?? 'none'}`,