sidekick-shared 0.26.0 → 0.26.1

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,14 @@ 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.1] - 2026-09-05
9
+
10
+ ### Fixed
11
+
12
+ - 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.
13
+ - `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.
14
+ - Codex quota failures name Codex instead of Claude or Anthropic. Default cache preferences and API-disabled resolution are unchanged.
15
+
8
16
  ## [0.26.0] - 2026-09-05
9
17
 
10
18
  ### Added
package/README.md CHANGED
@@ -9,7 +9,7 @@ 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
+ - **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
13
  - **Host-safe APIs (0.25.0)** — `listSessionPreviewsAsync()` / `readSessionPreviewAsync()` read previews with bounded concurrency and cooperative yielding; `ObservedSessionCollector.subscribe()` and `SessionMonitor.subscribe()` replace host polling loops with debounced change batches; `createSessionProviders({ onDiagnostic })` constructs every usable provider with structured diagnostics instead of throwing; `findSessionById()` resolves one session without scanning; `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` and `registerModelAlias()` transfer context/pricing resolutions across realms; `onAccountsChanged()` reports login/logout/switch events; provider constructors perform no environment I/O, and quota pollers stay dormant without an account.
14
14
  - **Native session-file helpers** — `encodeClaudeWorkspacePath()` / `getClaudeSessionDirectory()` locate Claude Code's `~/.claude/projects/` directories with the real on-disk encoding, `findCodexRolloutFile()` resolves a Codex session id to its rollout path, `readCodexHistory()` tail-reads `~/.codex/history.jsonl`, `listSessionPreviews()` / `readSessionPreview()` build a cheap stat-first recent-sessions index across providers, and `parseMcpToolName()` splits `mcp__<server>__<tool>` identifiers (browser-safe).
15
15
  - **Non-blocking account APIs & locked sync writers** — every login/switch entry point gains an async variant (`switchAccountAsync()`, `getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `prepareCodexAccountAsync()`, `finalizeCodexAccountAsync()`, `switchToCodexAccountAsync()`) that keeps `codex` CLI probes off the event loop, and synchronous callers get the same cross-process store lock via `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()`, with progress-based timeouts and abandoned-lock reclaim.
@@ -475,24 +475,25 @@ if (!peak.unavailable && peak.isPeak) {
475
475
  }
476
476
  ```
477
477
 
478
- ### Resolve quota with one precedence
478
+ ### Resolve quota with shared source policies
479
479
 
480
480
  ```typescript
481
481
  import { resolveQuota } from 'sidekick-shared';
482
482
 
483
- // Fresh persisted sample (status line, session, or API; under five minutes) →
483
+ // Default: fresh persisted sample (status line, session, or API; under five minutes) →
484
484
  // session logs (Codex rollouts) → provider API → older sample labelled by age.
485
485
  const quota = await resolveQuota({ providerId: 'claude-code' });
486
486
  console.log(quota.resolution); // 'snapshot-fresh' | 'session' | 'api' | 'snapshot-aging' | 'snapshot-stale' | 'unavailable'
487
487
  console.log(quota.source, quota.capturedSource, quota.freshness, quota.ageMs);
488
488
 
489
- // `--refresh` semantics: ask the API first.
489
+ // Ask the API before any local sample: used by every one-shot Codex query
490
+ // and by --refresh for other providers. API failures retain local fallbacks.
490
491
  await resolveQuota({ providerId: 'codex', workspacePath: process.cwd(), preferFresh: false });
491
492
  // Paint from local samples only (a poller will fetch live data next).
492
493
  await resolveQuota({ providerId: 'claude-code', allowApi: false });
493
494
  ```
494
495
 
495
- `resolveQuota()` is what `sidekick quota`, `quota --all`, the MCP `get_quota_status` tool, and both dashboards call, so every surface reports the same numbers and the same `resolution`. Every side effect is injectable (`readSnapshot`, `writeSnapshot`, `fetchImpl`, `getClaudeAccessToken`, the account resolvers) for tests.
496
+ `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
497
 
497
498
  ### Fetch subscription quota
498
499
 
@@ -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,
@@ -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'}`,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `sidekick quota`, `quota --all`, the MCP facts server, and both dashboards
5
5
  * used to pick between the live API, session logs, and the persisted snapshot
6
- * in four different orders. `resolveQuota()` applies one precedence:
6
+ * in four different orders. `resolveQuota()` defaults to this precedence:
7
7
  *
8
8
  * 1. a fresh persisted snapshot (younger than `QUOTA_FRESH_MAX_AGE_MS`, whatever
9
9
  * its origin: the official status-line sample, a session log, or an earlier
@@ -14,7 +14,9 @@
14
14
  * 4. an aging or stale snapshot, labelled as such, with the API failure attached;
15
15
  * 5. an unavailable state describing why nothing could be resolved.
16
16
  *
17
- * `preferFresh: false` skips step 1 (the `--refresh` flag). Every result carries
17
+ * `preferFresh: false` asks the API first when allowed (the `--refresh` flag
18
+ * and one-shot Codex queries). On failure, the newer session or persisted
19
+ * sample wins, with ties going to the persisted sample. Every result carries
18
20
  * `source`, `capturedSource`, `freshness`, `ageMs`, and a `resolution` tag.
19
21
  */
20
22
  import type { ResolvedActiveAccount, SavedAccountProfile } from './accountRegistry';
@@ -38,9 +40,9 @@ export interface ResolveQuotaOptions<T extends QuotaResolveProviderId = QuotaRes
38
40
  accountId?: string;
39
41
  /** Workspace whose Codex rollouts are searched first for session-derived rate limits. */
40
42
  workspacePath?: string;
41
- /** Use a fresh persisted snapshot before anything else (default true). `--refresh` passes false. */
43
+ /** Prefer local samples (default true). False asks the API before any local sample when allowed. */
42
44
  preferFresh?: boolean;
43
- /** Allow a provider API call when no fresh local sample exists (default true). */
45
+ /** Allow provider API calls (default true), including when preferFresh is false. */
44
46
  allowApi?: boolean;
45
47
  /** Re-point a stale saved-account pointer while resolving the account (default true). */
46
48
  selfHeal?: boolean;
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * `sidekick quota`, `quota --all`, the MCP facts server, and both dashboards
6
6
  * used to pick between the live API, session logs, and the persisted snapshot
7
- * in four different orders. `resolveQuota()` applies one precedence:
7
+ * in four different orders. `resolveQuota()` defaults to this precedence:
8
8
  *
9
9
  * 1. a fresh persisted snapshot (younger than `QUOTA_FRESH_MAX_AGE_MS`, whatever
10
10
  * its origin: the official status-line sample, a session log, or an earlier
@@ -15,7 +15,9 @@
15
15
  * 4. an aging or stale snapshot, labelled as such, with the API failure attached;
16
16
  * 5. an unavailable state describing why nothing could be resolved.
17
17
  *
18
- * `preferFresh: false` skips step 1 (the `--refresh` flag). Every result carries
18
+ * `preferFresh: false` asks the API first when allowed (the `--refresh` flag
19
+ * and one-shot Codex queries). On failure, the newer session or persisted
20
+ * sample wins, with ties going to the persisted sample. Every result carries
19
21
  * `source`, `capturedSource`, `freshness`, `ageMs`, and a `resolution` tag.
20
22
  */
21
23
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -146,6 +148,10 @@ function snapshotResolution(state) {
146
148
  return 'snapshot-aging';
147
149
  return 'snapshot-stale';
148
150
  }
151
+ function captureTimeMs(state) {
152
+ const capturedMs = state.capturedAt ? Date.parse(state.capturedAt) : NaN;
153
+ return Number.isFinite(capturedMs) ? capturedMs : Number.NEGATIVE_INFINITY;
154
+ }
149
155
  /**
150
156
  * Resolve the current quota for one provider with the shared precedence
151
157
  * described at the top of this file.
@@ -160,6 +166,7 @@ async function resolveQuota(options) {
160
166
  const capturedAt = now.toISOString();
161
167
  const preferFresh = options.preferFresh ?? true;
162
168
  const allowApi = options.allowApi ?? true;
169
+ const apiFirst = !preferFresh && allowApi;
163
170
  const readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
164
171
  const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
165
172
  const identity = resolveIdentity(options);
@@ -178,19 +185,27 @@ async function resolveQuota(options) {
178
185
  if (preferFresh && snapshot && snapshot.freshness === 'fresh') {
179
186
  return finish(snapshot, 'snapshot-fresh');
180
187
  }
181
- if (providerId === 'codex') {
182
- const local = (0, codexQuota_1.resolveCodexQuotaFromLocalSources)({
183
- workspacePath: options.workspacePath,
184
- provider: options.codexProvider,
185
- activeAccount: identity.codexAccount,
186
- codexHome: options.codexHome,
187
- // Only rollout hits count as "session-derived"; the snapshot fallback is
188
- // handled below so it can be labelled by age.
189
- readSnapshot: () => null,
190
- writeSnapshot,
191
- });
192
- if (local)
193
- return finish(local, 'session');
188
+ const readLocalCodex = () => (0, codexQuota_1.resolveCodexQuotaFromLocalSources)({
189
+ workspacePath: options.workspacePath,
190
+ provider: options.codexProvider,
191
+ activeAccount: identity.codexAccount,
192
+ codexHome: options.codexHome,
193
+ // Only rollout hits count as "session-derived"; the snapshot fallback is
194
+ // handled below so it can be labelled by age.
195
+ readSnapshot: () => null,
196
+ // Inspect candidates without overwriting the persisted fallback. Only
197
+ // the selected session sample is written, using the resolved account key.
198
+ writeSnapshot: () => { },
199
+ });
200
+ const finishLocal = (local) => {
201
+ if (identity.accountId)
202
+ writeSnapshot('codex', identity.accountId, local);
203
+ return finish(local, 'session');
204
+ };
205
+ if (providerId === 'codex' && !apiFirst) {
206
+ const local = readLocalCodex();
207
+ if (local?.available)
208
+ return finishLocal(local);
194
209
  }
195
210
  let apiState = null;
196
211
  if (allowApi) {
@@ -202,6 +217,12 @@ async function resolveQuota(options) {
202
217
  return finish(apiState, 'api');
203
218
  }
204
219
  }
220
+ if (providerId === 'codex' && apiFirst) {
221
+ const local = readLocalCodex();
222
+ if (local?.available && (!snapshot || captureTimeMs(local) > captureTimeMs(snapshot))) {
223
+ return { ...finishLocal(local), failure: (0, quotaPresentation_1.describeQuotaFailure)(apiState) };
224
+ }
225
+ }
205
226
  if (snapshot) {
206
227
  const fallback = finish(snapshot, snapshotResolution(snapshot));
207
228
  return apiState ? { ...fallback, failure: (0, quotaPresentation_1.describeQuotaFailure)(apiState) } : fallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "keywords": [
6
6
  "claude-code",