@cortexkit/anthropic-auth-core 1.23.0 → 2.0.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.
Files changed (136) hide show
  1. package/README.md +18 -1
  2. package/dist/accounts.d.ts +4 -163
  3. package/dist/cachekeep.d.ts +9 -0
  4. package/dist/claude-code.d.ts +16 -0
  5. package/dist/claustrum-enrollment.d.ts +2 -0
  6. package/dist/claustrum.d.ts +0 -209
  7. package/dist/commands/account.d.ts +2 -3
  8. package/dist/index.d.ts +45 -4
  9. package/dist/index.js +27569 -38
  10. package/dist/internal-types/LICENSE +21 -0
  11. package/dist/internal-types/commands/builtins.d.ts +71 -0
  12. package/dist/internal-types/commands/index.d.ts +10 -0
  13. package/dist/internal-types/commands/menu.d.ts +61 -0
  14. package/dist/internal-types/commands/model.d.ts +238 -0
  15. package/dist/internal-types/commands/pi.d.ts +23 -0
  16. package/dist/internal-types/commands/seam.d.ts +72 -0
  17. package/dist/internal-types/fs/refresh-file-lock.d.ts +33 -0
  18. package/dist/internal-types/logger/capture-sink.d.ts +14 -0
  19. package/dist/internal-types/logger/engine.d.ts +98 -0
  20. package/dist/internal-types/logger/index.d.ts +6 -0
  21. package/dist/internal-types/logger/redact.d.ts +11 -0
  22. package/dist/internal-types/pi-slot/index.d.ts +3 -0
  23. package/dist/internal-types/pi-slot/slot.d.ts +51 -0
  24. package/dist/internal-types/quota/codec.d.ts +11 -0
  25. package/dist/internal-types/quota/format.d.ts +46 -0
  26. package/dist/internal-types/quota/index.d.ts +10 -0
  27. package/dist/internal-types/quota/map.d.ts +63 -0
  28. package/dist/internal-types/quota/merge.d.ts +48 -0
  29. package/dist/internal-types/quota/projection.d.ts +64 -0
  30. package/dist/internal-types/store/attribution.d.ts +28 -0
  31. package/dist/internal-types/store/errors.d.ts +55 -0
  32. package/dist/internal-types/store/hooks.d.ts +12 -0
  33. package/dist/internal-types/store/identity.d.ts +39 -0
  34. package/dist/internal-types/store/index.d.ts +21 -0
  35. package/dist/internal-types/store/mutate.d.ts +189 -0
  36. package/dist/internal-types/store/pool.d.ts +197 -0
  37. package/dist/internal-types/store/provider-state.d.ts +131 -0
  38. package/dist/internal-types/store/publication.d.ts +47 -0
  39. package/dist/internal-types/store/pull.d.ts +36 -0
  40. package/dist/internal-types/store/refresh-lock.d.ts +67 -0
  41. package/dist/internal-types/store/refresh.d.ts +72 -0
  42. package/dist/internal-types/store/rows.d.ts +313 -0
  43. package/dist/internal-types/store/runtime.d.ts +36 -0
  44. package/dist/internal-types/store/schema.d.ts +443 -0
  45. package/dist/internal-types/store/settings.d.ts +63 -0
  46. package/dist/internal-types/store/torn.d.ts +78 -0
  47. package/dist/migration-source.d.ts +33 -0
  48. package/dist/models.d.ts +41 -0
  49. package/dist/native-account-runtime.d.ts +121 -0
  50. package/dist/native-account-view.d.ts +126 -0
  51. package/dist/native-command-effects.d.ts +59 -0
  52. package/dist/native-credential-validation.d.ts +42 -0
  53. package/dist/native-custody-activation.d.ts +45 -0
  54. package/dist/native-custody.d.ts +114 -0
  55. package/dist/native-enrollment-authority.d.ts +2 -0
  56. package/dist/native-host-auth-read.d.ts +3 -0
  57. package/dist/native-host-auth-write.d.ts +51 -0
  58. package/dist/native-host-auth.d.ts +12 -0
  59. package/dist/native-local-credential-service.d.ts +76 -0
  60. package/dist/native-local-runtime-readers.d.ts +22 -0
  61. package/dist/native-lock-observer.d.ts +12 -0
  62. package/dist/native-menu-executor.d.ts +243 -0
  63. package/dist/native-menu-model.d.ts +142 -0
  64. package/dist/native-migration-proof.d.ts +28 -0
  65. package/dist/native-migration-source.d.ts +47 -0
  66. package/dist/native-migration.d.ts +66 -0
  67. package/dist/native-quota-codec.d.ts +32 -0
  68. package/dist/native-refresh-coordinator.d.ts +240 -0
  69. package/dist/native-roster-seed.d.ts +115 -0
  70. package/dist/native-runtime-registry.d.ts +19 -0
  71. package/dist/native-runtime.d.ts +204 -0
  72. package/dist/native-settings.d.ts +6 -0
  73. package/dist/native-transient-account.d.ts +38 -0
  74. package/dist/native-ui.d.ts +45 -0
  75. package/dist/native-usage-publication.d.ts +12 -0
  76. package/dist/native-vault-admission.d.ts +8 -0
  77. package/dist/native-vault-connection.d.ts +33 -0
  78. package/dist/native-vault-paths.d.ts +13 -0
  79. package/dist/native-vault-runtime.d.ts +102 -0
  80. package/dist/native-vault-seed.d.ts +16 -0
  81. package/dist/pool-authority.d.ts +159 -0
  82. package/dist/pool-binding.d.ts +32 -0
  83. package/dist/pool-paths.d.ts +21 -0
  84. package/dist/pool-store.d.ts +32 -0
  85. package/dist/provider-error.d.ts +139 -0
  86. package/dist/quota-manager.d.ts +18 -7
  87. package/dist/relay.d.ts +14 -1
  88. package/dist/request-history.d.ts +34 -0
  89. package/dist/routing.d.ts +1 -2
  90. package/dist/sse.d.ts +81 -0
  91. package/dist/sticky-routing.d.ts +28 -0
  92. package/package.json +10 -4
  93. package/dist/accounts.js +0 -3881
  94. package/dist/auth.js +0 -185
  95. package/dist/cache1h.js +0 -97
  96. package/dist/cachekeep-registry.js +0 -124
  97. package/dist/cachekeep.js +0 -604
  98. package/dist/cch.js +0 -172
  99. package/dist/claude-code.js +0 -403
  100. package/dist/claustrum-enrollment-registry.d.ts +0 -17
  101. package/dist/claustrum-enrollment-registry.js +0 -194
  102. package/dist/claustrum-enrollment.js +0 -485
  103. package/dist/claustrum-scoped-roster.d.ts +0 -23
  104. package/dist/claustrum-scoped-roster.js +0 -194
  105. package/dist/claustrum-scoped-runtime.d.ts +0 -28
  106. package/dist/claustrum-scoped-runtime.js +0 -230
  107. package/dist/claustrum-scoped.d.ts +0 -39
  108. package/dist/claustrum-scoped.js +0 -193
  109. package/dist/claustrum.js +0 -1471
  110. package/dist/commands/account.js +0 -368
  111. package/dist/constants.js +0 -83
  112. package/dist/custom-headers.js +0 -102
  113. package/dist/dump.js +0 -616
  114. package/dist/fast.js +0 -66
  115. package/dist/json.js +0 -10
  116. package/dist/killswitch.js +0 -132
  117. package/dist/logger.js +0 -288
  118. package/dist/logging.js +0 -59
  119. package/dist/mid-conversation-output-config.js +0 -89
  120. package/dist/model-remap.js +0 -72
  121. package/dist/models.js +0 -177
  122. package/dist/network-errors.js +0 -27
  123. package/dist/oauth-profile.js +0 -60
  124. package/dist/pkce.js +0 -17
  125. package/dist/prime.js +0 -841
  126. package/dist/provider.js +0 -0
  127. package/dist/quota-header-feed.js +0 -331
  128. package/dist/quota-headers.js +0 -121
  129. package/dist/quota-manager.js +0 -856
  130. package/dist/quotas.js +0 -141
  131. package/dist/relay.js +0 -1303
  132. package/dist/routing.js +0 -94
  133. package/dist/start.js +0 -29
  134. package/dist/sticky-routing.js +0 -541
  135. package/dist/thinking-binding.js +0 -54
  136. package/dist/token-fingerprint.js +0 -8
package/dist/prime.js DELETED
@@ -1,841 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { realpathSync } from 'node:fs';
3
- import { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { join, resolve } from 'node:path';
6
- import { isOAuthAccount, isPermanentRefreshError, killswitchPassesPolicy, } from './accounts.js';
7
- import { logger } from './logger.js';
8
- import { CLAUDE_HAIKU_4_5_MODEL_ID, CLAUDE_HAIKU_4_5_PRICING, } from './models.js';
9
- // Re-export pricing for downstream packages that import it through core's
10
- // public surface (the test suite verifies exact per-million-token values).
11
- export { CLAUDE_HAIKU_4_5_PRICING };
12
- /**
13
- * `/claude-prime` — opt-in 5h quota window priming. Default OFF.
14
- *
15
- * Every OAuth account (main + enabled fallbacks) gets one minimal request
16
- * fired ~60s after its `five_hour.resetsAt`, exactly once across concurrent
17
- * OpenCode processes. Catch-up on boot; redundancy guard via fresh quota
18
- * check; cross-process atomic marker claim via `writeFile(..., flag: 'wx')`.
19
- */
20
- export const CLAUDE_PRIME_COMMAND_NAME = 'claude-prime';
21
- export const PRIME_TICK_MS = 60_000;
22
- export const PRIME_DUE_OFFSET_MS = 60_000;
23
- export const PRIME_MARKER_MAX_AGE_MS = 6 * 60 * 60_000;
24
- export const PRIME_POST_FIRE_REFRESH_MS = 90_000;
25
- /** Minimum interval between forced quota checks for an unchanged reset epoch. */
26
- export const PRIME_CHECK_THROTTLE_MS = 5 * 60_000;
27
- /**
28
- * Pure command parser — accepts `''` / `on` / `off` and treats anything else
29
- * as a usage error. Mirrors `parseFastModeCommandAction` shape so all
30
- * `claude-*` commands stay uniform.
31
- */
32
- export function parsePrimeCommandAction(input) {
33
- const normalized = input.trim().split(/\s+/).filter(Boolean);
34
- if (normalized.length === 0)
35
- return { type: 'status' };
36
- if (normalized.length === 1 && normalized[0] === 'on')
37
- return { type: 'enable' };
38
- if (normalized.length === 1 && normalized[0] === 'off')
39
- return { type: 'disable' };
40
- return { type: 'usage' };
41
- }
42
- /**
43
- * Per-million-token USD cost projection. Locale-independent math; the display
44
- * layer is responsible for formatting. Returns 0 for absent counters so
45
- * pre-fire accounts still get a clean number in the sidebar/dialog.
46
- */
47
- export function estimatePrimeCostUsd(usage) {
48
- if (!usage)
49
- return 0;
50
- return ((usage.inputTokens * CLAUDE_HAIKU_4_5_PRICING.input +
51
- usage.outputTokens * CLAUDE_HAIKU_4_5_PRICING.output) /
52
- 1_000_000);
53
- }
54
- function accountLabel(id, storage, fallbackAccount) {
55
- if (fallbackAccount?.label?.trim())
56
- return fallbackAccount.label;
57
- if (id === 'main')
58
- return 'main';
59
- // Stale-storage fallback: the caller passed a non-empty storage so prefer
60
- // the first matching account label.
61
- const found = storage?.accounts?.find((a) => a.id === id);
62
- if (found?.label?.trim())
63
- return found.label;
64
- return id;
65
- }
66
- /**
67
- * Project per-account prime status. Enumerates a synthetic `main` plus each
68
- * enabled OAuth fallback. `nextDueAt` is the stored five-hour reset plus
69
- * `PRIME_DUE_OFFSET_MS`; null when no reset is known. Manager-transient
70
- * `lastPrimedAt`/`lastResult` overlay the persisted counters so the dialog and
71
- * sidebar can show "just attempted" without waiting for the next save.
72
- */
73
- export function buildPrimeAccountStatuses(storage, options) {
74
- const transient = options?.transient;
75
- const mainStoredQuota = storage?.quota?.mainQuota;
76
- const mainUsage = storage?.prime?.main;
77
- const mainStatus = {
78
- id: 'main',
79
- label: 'main',
80
- nextDueAt: primeNextDueAt(mainStoredQuota?.five_hour),
81
- lastPrimedAt: transient?.get('main')?.lastPrimedAt ?? null,
82
- lastResult: transient?.get('main')?.lastResult,
83
- usage: mainUsage,
84
- estimatedCostUsd: estimatePrimeCostUsd(mainUsage),
85
- };
86
- const fallbacks = (storage?.accounts ?? [])
87
- .filter(isOAuthAccount)
88
- .filter((account) => account.id !== 'main')
89
- .filter((account) => account.enabled !== false)
90
- .map((account) => ({
91
- id: account.id,
92
- label: accountLabel(account.id, storage, account),
93
- nextDueAt: primeNextDueAt(account.quota?.five_hour),
94
- lastPrimedAt: transient?.get(account.id)?.lastPrimedAt ?? null,
95
- lastResult: transient?.get(account.id)?.lastResult,
96
- usage: account.prime,
97
- estimatedCostUsd: estimatePrimeCostUsd(account.prime),
98
- }));
99
- return [mainStatus, ...fallbacks];
100
- }
101
- function primeNextDueAt(window) {
102
- if (!window?.resetsAt)
103
- return undefined;
104
- const ms = Date.parse(window.resetsAt);
105
- if (!Number.isFinite(ms))
106
- return undefined;
107
- return ms + PRIME_DUE_OFFSET_MS;
108
- }
109
- /**
110
- * Pure multi-line status summary shared by the OpenCode dialog, the sidebar
111
- * expanded row, and the Pi display. Title is stable text the dialog uses to
112
- * anchor and the orchestrator uses to assert RED.
113
- */
114
- export function buildPrimeStatusSummary(input) {
115
- const header = input.enabled
116
- ? '## Claude Prime Status'
117
- : '## Claude Prime Disabled';
118
- const lines = [header, ''];
119
- lines.push(`Status: ${input.enabled ? 'enabled' : 'disabled'}`);
120
- lines.push('');
121
- lines.push('Accounts:');
122
- for (const account of input.accounts) {
123
- const usage = account.usage;
124
- const count = usage?.count ?? 0;
125
- const due = formatDue(account.nextDueAt);
126
- const primed = formatPrimed(account.lastPrimedAt, account.lastResult);
127
- const segments = [`${account.label}`];
128
- if (due)
129
- segments.push(`next prime ${due}`);
130
- if (primed)
131
- segments.push(primed);
132
- if (count > 0) {
133
- segments.push(`${count} ${count === 1 ? 'prime' : 'primes'} \u2248 $${formatUsd(account.estimatedCostUsd ?? 0)}`);
134
- }
135
- lines.push(`- ${segments.join(' · ')}`);
136
- }
137
- lines.push('');
138
- lines.push('Usage: `/claude-prime on`, `/claude-prime off`, or status.');
139
- return lines.join('\n');
140
- }
141
- function formatDue(nextDueAt) {
142
- if (typeof nextDueAt !== 'number')
143
- return '';
144
- return new Date(nextDueAt).toLocaleTimeString([], {
145
- hour: '2-digit',
146
- minute: '2-digit',
147
- });
148
- }
149
- function formatPrimed(lastPrimedAt, lastResult) {
150
- if (typeof lastPrimedAt !== 'number')
151
- return '';
152
- const time = new Date(lastPrimedAt).toLocaleTimeString([], {
153
- hour: '2-digit',
154
- minute: '2-digit',
155
- });
156
- if (lastResult === 'error')
157
- return `primed ${time} err`;
158
- if (lastResult === 'skipped')
159
- return `primed ${time} skipped`;
160
- return `primed ${time} \u2713`;
161
- }
162
- function formatUsd(value) {
163
- if (value === 0)
164
- return '0';
165
- if (value < 0.0001)
166
- return value.toExponential(2);
167
- return value.toFixed(Math.min(6, Math.max(0, 4)));
168
- }
169
- /**
170
- * Pure executor — returns markdown text and an optional `updated` envelope.
171
- * Effects (persistent toggle, manager start/stop) are the caller's job; Pi
172
- * uses this same function in display-only mode.
173
- */
174
- export function executePrimeCommand(input) {
175
- const action = parsePrimeCommandAction(input.argumentsText);
176
- if (action.type === 'status') {
177
- return {
178
- text: buildPrimeStatusSummary({
179
- enabled: input.enabled,
180
- accounts: input.accounts,
181
- }),
182
- };
183
- }
184
- if (action.type === 'enable') {
185
- return {
186
- text: buildPrimeStatusSummary({
187
- enabled: true,
188
- accounts: input.accounts,
189
- }),
190
- updated: { enabled: true },
191
- };
192
- }
193
- if (action.type === 'disable') {
194
- return {
195
- text: buildPrimeStatusSummary({
196
- enabled: false,
197
- accounts: input.accounts,
198
- }),
199
- updated: { enabled: false },
200
- };
201
- }
202
- return {
203
- text: [
204
- '## Claude Prime Usage',
205
- '',
206
- 'Usage: `/claude-prime`, `/claude-prime on`, or `/claude-prime off`.',
207
- '',
208
- buildPrimeStatusSummary({
209
- enabled: input.enabled,
210
- accounts: input.accounts,
211
- }),
212
- ].join('\n'),
213
- };
214
- }
215
- const ELIGIBILITY_REASON_LABEL = {
216
- 'not-oauth': 'API-key account',
217
- disabled: 'disabled',
218
- 'needs-reauth': 'needs re-login',
219
- killswitch: 'killswitch',
220
- };
221
- /**
222
- * Eligibility check shared by the manager tick and the dialog preview.
223
- * Returns a discriminated result so the manager can log a specific reason
224
- * per the spec's Logging table. Quota policy is evaluated authoritatively on
225
- * the fresh snapshot immediately before claim.
226
- */
227
- export function primeIsEligible(input) {
228
- if (!input.isOAuth)
229
- return { eligible: false, reason: 'not-oauth' };
230
- if (!input.isEnabled)
231
- return { eligible: false, reason: 'disabled' };
232
- if (input.hasPermanentRefreshError)
233
- return { eligible: false, reason: 'needs-reauth' };
234
- return { eligible: true };
235
- }
236
- export function primeEligibilityReasonLabel(reason) {
237
- return ELIGIBILITY_REASON_LABEL[reason];
238
- }
239
- export const PRIME_REQUEST_BODY = {
240
- model: CLAUDE_HAIKU_4_5_MODEL_ID,
241
- max_tokens: 1,
242
- system: 'Reply with 1 when you receive 0.',
243
- messages: [{ role: 'user', content: '0' }],
244
- };
245
- export function buildPrimeRequestBody() {
246
- // Copy to keep callers from accidentally mutating the canonical frozen body.
247
- return JSON.parse(JSON.stringify(PRIME_REQUEST_BODY));
248
- }
249
- function defaultMarkerDir() {
250
- return join(tmpdir(), 'opencode-anthropic-auth', 'prime');
251
- }
252
- export function primeStorageFingerprint(storagePath) {
253
- const absolutePath = resolve(storagePath);
254
- let canonicalPath;
255
- try {
256
- canonicalPath = realpathSync(absolutePath);
257
- }
258
- catch (error) {
259
- if (error.code !== 'ENOENT')
260
- throw error;
261
- canonicalPath = absolutePath;
262
- }
263
- return createHash('sha256').update(canonicalPath).digest('hex').slice(0, 12);
264
- }
265
- export function primeMarkerNamespaceDir(markerDir, storagePath) {
266
- // Legacy root markers are intentionally not migrated: an upgrade may send
267
- // one extra Haiku request per account, whose usage is measured from response
268
- // accounting, then the namespaced claim self-heals.
269
- return join(markerDir, primeStorageFingerprint(storagePath));
270
- }
271
- export function primeAccountMarkerDir(markerDir, storagePath, accountFingerprint) {
272
- if (!/^[a-f0-9]{12,}$/i.test(accountFingerprint)) {
273
- throw new Error('Prime account fingerprint must be a hexadecimal digest');
274
- }
275
- return join(primeMarkerNamespaceDir(markerDir, storagePath), accountFingerprint.toLowerCase().slice(0, 12));
276
- }
277
- export function primeMarkerPath(markerDir, accountId, resetsAtEpochMs) {
278
- return join(markerDir, `${encodeURIComponent(accountId)}-${resetsAtEpochMs}`);
279
- }
280
- function primeBootstrapMarkerPath(markerDir, accountId) {
281
- return join(markerDir, `${encodeURIComponent(accountId)}-bootstrap`);
282
- }
283
- function primeAccountLabel(id, fallback) {
284
- if (id === 'main')
285
- return 'main';
286
- if (fallback && 'label' in fallback && fallback.label?.trim()) {
287
- return fallback.label;
288
- }
289
- return id;
290
- }
291
- /**
292
- * Build the list of accounts the manager should evaluate EVERY tick.
293
- * Includes disabled and API-key accounts so the eligibility log fires
294
- * for them too (the spec Logging table mandates the debug log for
295
- * every non-eligible case so operators can troubleshoot). Filter for
296
- * the actual fire decision happens via `primeIsEligible`.
297
- */
298
- function evaluateAccounts(storage) {
299
- const evaluations = [];
300
- const mainRefreshError = storage.refresh?.mainLastRefreshError;
301
- evaluations.push({
302
- id: 'main',
303
- label: 'main',
304
- isOAuth: true,
305
- isEnabled: true,
306
- hasPermanentRefreshError: isPermanentRefreshError(mainRefreshError),
307
- storedQuota: storage.quota?.mainQuota,
308
- storedFiveHour: storage.quota?.mainQuota?.five_hour,
309
- });
310
- for (const account of storage.accounts ?? []) {
311
- if (account.id === 'main') {
312
- logger.debug('prime', 'ineligible', {
313
- account: primeAccountLabel(account.id, account),
314
- reason: 'reserved-id',
315
- });
316
- continue;
317
- }
318
- if (!isOAuthAccount(account)) {
319
- // API-key accounts are surfaced here so the eligibility-skip debug
320
- // log fires once per tick and an operator can confirm the
321
- // account won't prime. The check itself is in `primeIsEligible`.
322
- evaluations.push({
323
- id: account.id,
324
- label: primeAccountLabel(account.id, account),
325
- isOAuth: false,
326
- isEnabled: account.enabled !== false,
327
- hasPermanentRefreshError: false,
328
- });
329
- continue;
330
- }
331
- evaluations.push({
332
- id: account.id,
333
- label: primeAccountLabel(account.id, account),
334
- isOAuth: true,
335
- isEnabled: account.enabled !== false,
336
- hasPermanentRefreshError: isPermanentRefreshError(account.lastRefreshError),
337
- storedQuota: account.quota,
338
- storedFiveHour: account.quota?.five_hour,
339
- });
340
- }
341
- return evaluations;
342
- }
343
- function storedResetMs(window) {
344
- if (!window?.resetsAt)
345
- return undefined;
346
- const ms = Date.parse(window.resetsAt);
347
- return Number.isFinite(ms) ? ms : undefined;
348
- }
349
- export class PrimeManager {
350
- timer = null;
351
- setIntervalImpl;
352
- clearIntervalImpl;
353
- setTimeoutImpl;
354
- clearTimeoutImpl;
355
- postFireRefreshTimers = new Set();
356
- lastForcedCheck = new Map();
357
- // Set true by `stop()` so a tick already in flight can short-circuit
358
- // before claim/send, preventing a stale in-flight cycle from firing
359
- // after `/claude-prime off` or after a newer plugin invocation has
360
- // replaced this instance (M3b).
361
- stopped = false;
362
- // The registry replaces this dependency bundle on plugin reload so the
363
- // singleton manager cannot retain closures from an obsolete plugin context.
364
- options;
365
- // Per-account transient state from the last ATTEMPT — overlays persisted
366
- // counters in `stats()` so the sidebar/dialog can show "just attempted"
367
- // without waiting for the next save. NOT persisted.
368
- //
369
- // Crucially, fresh-check skips do NOT touch this map. Only an actual
370
- // claim+fire attempt mutates it. See `runForAccount` for the invariant
371
- // (M4): skip is not the same as a successful prime.
372
- transient = new Map();
373
- // Latest cumulative counters returned by recordSuccess. Overlays the
374
- // persisted counters in stats() before the next load persists them.
375
- counters = new Map();
376
- // Per-account "window active" observation from the most recent fresh
377
- // check (M4). Surfaced in the status as the rendered "— window active"
378
- // text; independent of lastPrimedAt/lastResult.
379
- windowActive = new Map();
380
- constructor(options) {
381
- this.options = options;
382
- this.setIntervalImpl = options.setIntervalImpl ?? globalThis.setInterval;
383
- this.clearIntervalImpl =
384
- options.clearIntervalImpl ?? globalThis.clearInterval;
385
- this.setTimeoutImpl = options.setTimeoutImpl ?? globalThis.setTimeout;
386
- this.clearTimeoutImpl = options.clearTimeoutImpl ?? globalThis.clearTimeout;
387
- }
388
- updateOptions(options) {
389
- this.options = options;
390
- }
391
- start() {
392
- if (this.timer)
393
- return;
394
- this.stopped = false;
395
- logger.trace('prime', 'manager started');
396
- this.timer = this.setIntervalImpl(() => {
397
- void this.tick().catch((error) => {
398
- logger.warn('prime', 'tick failed', {
399
- error: error instanceof Error ? error.message : String(error),
400
- });
401
- });
402
- }, PRIME_TICK_MS);
403
- if (typeof this.timer === 'object' && 'unref' in this.timer) {
404
- ;
405
- this.timer.unref?.();
406
- }
407
- }
408
- stop() {
409
- const hadTimer = this.timer !== null;
410
- const hadPostFireRefresh = this.postFireRefreshTimers.size > 0;
411
- this.stopped = true;
412
- if (this.timer) {
413
- this.clearIntervalImpl(this.timer);
414
- this.timer = null;
415
- }
416
- for (const timer of this.postFireRefreshTimers) {
417
- this.clearTimeoutImpl(timer);
418
- }
419
- this.postFireRefreshTimers.clear();
420
- this.lastForcedCheck.clear();
421
- if (!hadTimer && !hadPostFireRefresh)
422
- return;
423
- logger.trace('prime', 'manager stopped');
424
- }
425
- isStopped() {
426
- return this.stopped;
427
- }
428
- async tick() {
429
- const now = this.options.now?.() ?? Date.now();
430
- logger.trace('prime', 'tick start', { now });
431
- await this.sweepMarkers(now);
432
- if (this.stopped) {
433
- logger.trace('prime', 'tick: stopped, aborting');
434
- return;
435
- }
436
- // Re-load persisted state at the start of every tick. The previous
437
- // read may pre-date an `/claude-prime off` from another process or
438
- // a settings edit (M3b).
439
- const storage = await this.options.loadStorage();
440
- if (!storage) {
441
- logger.trace('prime', 'tick: no storage, skipping');
442
- return;
443
- }
444
- if (storage.prime?.enabled !== true) {
445
- logger.trace('prime', 'tick: feature disabled, skipping');
446
- return;
447
- }
448
- const evaluations = evaluateAccounts(storage);
449
- await Promise.all(evaluations.map((evaluation) => this.runForAccount(evaluation, storage, now)));
450
- logger.trace('prime', 'tick end');
451
- }
452
- /**
453
- * Project cumulative per-account status for the sidebar and dialog. Machine
454
- * values only — formatting belongs in the TUI/sidebar/Pi layer.
455
- */
456
- stats(storage, nowArg) {
457
- const baseStatuses = buildPrimeAccountStatuses(storage ?? null, {
458
- now: nowArg,
459
- transient: this.transient,
460
- });
461
- if (this.counters.size === 0)
462
- return baseStatuses;
463
- return baseStatuses.map((status) => {
464
- const overlay = this.counters.get(status.id);
465
- if (!overlay)
466
- return status;
467
- return {
468
- ...status,
469
- usage: overlay,
470
- estimatedCostUsd: estimatePrimeCostUsd(overlay),
471
- };
472
- });
473
- }
474
- /** True if the most recent fresh-check observed the window as already running. */
475
- isWindowActive(accountId) {
476
- return this.windowActive.has(accountId);
477
- }
478
- async runForAccount(evaluation, storage, now) {
479
- try {
480
- if (this.stopped)
481
- return;
482
- const eligibility = primeIsEligible({
483
- storage,
484
- accountId: evaluation.id,
485
- isOAuth: evaluation.isOAuth,
486
- isEnabled: evaluation.isEnabled,
487
- hasPermanentRefreshError: evaluation.hasPermanentRefreshError,
488
- quota: evaluation.storedQuota,
489
- });
490
- if (!eligibility.eligible) {
491
- // Per spec Logging table: every stored-state ineligible case (disabled /
492
- // needs-reauth / API-key) emits a debug log carrying
493
- // the account's display label and the specific reason. `trace`
494
- // would hide this from operators troubleshooting "why isn't prime
495
- // firing for my X account".
496
- logger.debug('prime', 'ineligible', {
497
- account: evaluation.label,
498
- reason: primeEligibilityReasonLabel(eligibility.reason),
499
- });
500
- return;
501
- }
502
- const storedResetEpoch = storedResetMs(evaluation.storedFiveHour);
503
- if (storedResetEpoch !== undefined &&
504
- now < storedResetEpoch + PRIME_DUE_OFFSET_MS) {
505
- // "Not due" is a routine tick outcome, not an operator-visible
506
- // diagnostic. Per the spec's `trace` row for tick evaluation.
507
- logger.trace('prime', 'not due', { account: evaluation.label });
508
- return;
509
- }
510
- const accountFingerprint = await this.options.getAccountFingerprint(evaluation.id);
511
- if (!accountFingerprint) {
512
- logger.debug('prime', 'credential identity unavailable', {
513
- account: evaluation.label,
514
- });
515
- return;
516
- }
517
- const claimIdentity = `${evaluation.id}:${accountFingerprint
518
- .toLowerCase()
519
- .slice(0, 12)}`;
520
- const markerDir = this.markerDir(accountFingerprint);
521
- const bootstrapMarkerPath = primeBootstrapMarkerPath(markerDir, evaluation.id);
522
- const expectedMarkerPath = storedResetEpoch === undefined
523
- ? bootstrapMarkerPath
524
- : primeMarkerPath(markerDir, evaluation.id, storedResetEpoch);
525
- try {
526
- await stat(expectedMarkerPath);
527
- // A completed claim makes another forced poll redundant; the normal
528
- // background refresh will persist the reset when the usage API catches up.
529
- return;
530
- }
531
- catch (error) {
532
- if (error.code !== 'ENOENT')
533
- throw error;
534
- }
535
- if (storedResetEpoch === undefined &&
536
- (await this.hasAnyClaimMarker(evaluation.id, accountFingerprint))) {
537
- // The bootstrap sentinel is replaced only after an epoch claim exists.
538
- // A stale process must observe one side of that transition.
539
- return;
540
- }
541
- if (this.stopped)
542
- return;
543
- // Binding the throttle to the observed epoch preserves the immediate
544
- // check when a newly passed reset replaces the previous window.
545
- const lastForcedCheck = this.lastForcedCheck.get(claimIdentity);
546
- if (lastForcedCheck !== undefined &&
547
- lastForcedCheck.resetEpoch === storedResetEpoch &&
548
- now < lastForcedCheck.at + PRIME_CHECK_THROTTLE_MS) {
549
- return;
550
- }
551
- this.lastForcedCheck.set(claimIdentity, {
552
- at: now,
553
- resetEpoch: storedResetEpoch,
554
- });
555
- let refreshed;
556
- try {
557
- refreshed = await this.options.refreshQuota(evaluation.id);
558
- }
559
- catch (error) {
560
- // Quota fresh-check failure: skip this tick without consuming a claim.
561
- logger.warn('prime', 'refresh failed', {
562
- account: evaluation.label,
563
- error: error instanceof Error ? error.message : String(error),
564
- });
565
- return;
566
- }
567
- // Re-check stop + persist opt-in after every await boundary so
568
- // an `/claude-prime off` (or a newer plugin instance's stop())
569
- // aborts the cycle BEFORE claim or send (M3b).
570
- if (this.stopped)
571
- return;
572
- const reloaded = await this.options.loadStorage();
573
- if (reloaded?.prime?.enabled !== true)
574
- return;
575
- const fresh = refreshed.quota;
576
- // Stale fresh-check: the adapter returned a cached snapshot (quota
577
- // API 429-backoff, another process owns the file lock, or the
578
- // account was just refreshed by someone else). Skip without
579
- // claiming — a cached past `resetsAt` would otherwise let us
580
- // prime against an already-started window. Retry next tick.
581
- if (!refreshed.fresh) {
582
- logger.debug('prime', 'stale fresh-check', {
583
- account: evaluation.label,
584
- });
585
- return;
586
- }
587
- if (!killswitchPassesPolicy(fresh, reloaded, evaluation.id, CLAUDE_HAIKU_4_5_MODEL_ID)) {
588
- logger.debug('prime', 'ineligible', {
589
- account: evaluation.label,
590
- reason: primeEligibilityReasonLabel('killswitch'),
591
- });
592
- return;
593
- }
594
- const freshResetEpoch = storedResetMs(fresh?.five_hour);
595
- if (freshResetEpoch !== undefined && freshResetEpoch > now) {
596
- // Window already started by a real request. Track the active-
597
- // window observation separately (M4) so the status renderer can
598
- // show "— window active" without conflating it with a successful
599
- // prime. Do NOT touch lastPrimedAt / lastResult.
600
- logger.debug('prime', 'window active', {
601
- account: evaluation.label,
602
- resetsAt: fresh.five_hour?.resetsAt,
603
- });
604
- this.windowActive.set(evaluation.id, now);
605
- return;
606
- }
607
- this.windowActive.delete(evaluation.id);
608
- const markerEpoch = freshResetEpoch ?? storedResetEpoch;
609
- const markerPath = markerEpoch === undefined
610
- ? await this.tryClaimBootstrap(evaluation.id, accountFingerprint)
611
- : await this.tryClaim(evaluation.id, accountFingerprint, markerEpoch);
612
- if (!markerPath) {
613
- logger.debug('prime', 'claim lost', {
614
- account: evaluation.label,
615
- marker: `${evaluation.id}-${markerEpoch ?? 'bootstrap'}`,
616
- });
617
- return;
618
- }
619
- const postClaimStorage = await this.options.loadStorage();
620
- if (this.stopped || postClaimStorage?.prime?.enabled !== true) {
621
- await rm(markerPath, { force: true });
622
- return;
623
- }
624
- if (markerEpoch !== undefined) {
625
- // Create the epoch claim before releasing the bootstrap sentinel so
626
- // concurrent processes never observe an unclaimed transition gap.
627
- await rm(bootstrapMarkerPath, { force: true });
628
- if (this.stopped) {
629
- await rm(markerPath, { force: true });
630
- return;
631
- }
632
- }
633
- await this.fire(evaluation, now, postClaimStorage);
634
- }
635
- catch (error) {
636
- logger.warn('prime', 'account evaluation failed', {
637
- account: evaluation.label,
638
- error: error instanceof Error ? error.message : String(error),
639
- });
640
- }
641
- }
642
- async tryClaim(accountId, accountFingerprint, resetsAtEpochMs) {
643
- const dir = this.markerDir(accountFingerprint);
644
- const markerPath = primeMarkerPath(dir, accountId, resetsAtEpochMs);
645
- return this.tryClaimPath(dir, markerPath);
646
- }
647
- async tryClaimBootstrap(accountId, accountFingerprint) {
648
- const dir = this.markerDir(accountFingerprint);
649
- const markerPath = primeBootstrapMarkerPath(dir, accountId);
650
- return this.tryClaimPath(dir, markerPath);
651
- }
652
- async tryClaimPath(dir, markerPath) {
653
- try {
654
- await mkdir(dir, { recursive: true });
655
- }
656
- catch {
657
- // Directory-create failures are propagated by the writeFile attempt below.
658
- }
659
- try {
660
- await writeFile(markerPath, '', { encoding: 'utf8', flag: 'wx' });
661
- return markerPath;
662
- }
663
- catch (error) {
664
- const code = error.code;
665
- if (code === 'EEXIST')
666
- return null;
667
- throw error;
668
- }
669
- }
670
- async hasAnyClaimMarker(accountId, accountFingerprint) {
671
- const dir = this.markerDir(accountFingerprint);
672
- const prefix = `${encodeURIComponent(accountId)}-`;
673
- try {
674
- return (await readdir(dir)).some((entry) => entry.startsWith(prefix));
675
- }
676
- catch (error) {
677
- if (error.code === 'ENOENT')
678
- return false;
679
- throw error;
680
- }
681
- }
682
- async fire(evaluation, now, _storage) {
683
- let result;
684
- try {
685
- result = await this.options.sendPrime(evaluation.id);
686
- }
687
- catch (error) {
688
- const message = error instanceof Error ? error.message : String(error);
689
- logger.warn('prime', 'prime fire threw', {
690
- account: evaluation.label,
691
- error: message,
692
- });
693
- this.transient.set(evaluation.id, {
694
- lastPrimedAt: now,
695
- lastResult: 'error',
696
- });
697
- return;
698
- }
699
- if (this.stopped)
700
- return;
701
- if (!result.ok) {
702
- // `vault-cold` is a cache-warm skip; it is not a provider failure.
703
- // - `prime token refresh failed` — token-refresh failure
704
- // before the request fires (reason: 'token-refresh').
705
- // - `prime fire failed` — HTTP error / fetch throw /
706
- // identity failure during the request (reason: 'send').
707
- if (result.reason === 'token-refresh') {
708
- logger.warn('prime', 'prime token refresh failed', {
709
- account: evaluation.label,
710
- error: result.error,
711
- });
712
- }
713
- else if (result.reason === 'vault-cold') {
714
- logger.debug('prime', 'prime vault credential cold', {
715
- account: evaluation.label,
716
- reason: result.reason,
717
- });
718
- }
719
- else {
720
- logger.warn('prime', 'prime fire failed', {
721
- account: evaluation.label,
722
- status: result.status,
723
- error: result.error,
724
- });
725
- }
726
- this.transient.set(evaluation.id, {
727
- lastPrimedAt: now,
728
- lastResult: result.reason === 'vault-cold' ? 'skipped' : 'error',
729
- });
730
- return;
731
- }
732
- const usage = {
733
- inputTokens: result.usage?.inputTokens,
734
- outputTokens: result.usage?.outputTokens,
735
- };
736
- try {
737
- const counters = await this.options.recordSuccess(evaluation.id, usage);
738
- if (this.stopped) {
739
- // If stopped mid-fire, roll back the transient so a later
740
- // `stats()` does not report a phantom success.
741
- this.transient.delete(evaluation.id);
742
- this.counters.delete(evaluation.id);
743
- return;
744
- }
745
- this.counters.set(evaluation.id, counters);
746
- this.transient.set(evaluation.id, {
747
- lastPrimedAt: now,
748
- lastResult: 'ok',
749
- });
750
- logger.info('prime', 'prime fired', {
751
- account: evaluation.label,
752
- status: result.status,
753
- ms: result.ms,
754
- usage: {
755
- inputTokens: counters.inputTokens,
756
- outputTokens: counters.outputTokens,
757
- },
758
- });
759
- this.schedulePostFireRefresh(evaluation);
760
- }
761
- catch (error) {
762
- // Persisting counters failed; surface the error but keep the marker so
763
- // the cycle counts as attempted. Operator can retry next reset epoch.
764
- logger.warn('prime', 'recordSuccess failed', {
765
- account: evaluation.label,
766
- error: error instanceof Error ? error.message : String(error),
767
- });
768
- this.transient.set(evaluation.id, {
769
- lastPrimedAt: now,
770
- lastResult: 'error',
771
- });
772
- }
773
- }
774
- schedulePostFireRefresh(evaluation) {
775
- // Unified fire-response headers can arm the window instantly on newer
776
- // trees. This branch predates header harvesting, so a delayed poll captures
777
- // the reset after the usage API's observed propagation lag.
778
- const timer = this.setTimeoutImpl(() => {
779
- this.postFireRefreshTimers.delete(timer);
780
- if (this.stopped)
781
- return;
782
- void (async () => {
783
- const storage = await this.options.loadStorage();
784
- if (this.stopped || storage?.prime?.enabled !== true)
785
- return;
786
- await this.options.refreshQuota(evaluation.id);
787
- })().catch((error) => {
788
- logger.debug('prime', 'post-fire refresh failed', {
789
- account: evaluation.label,
790
- error: error instanceof Error ? error.message : String(error),
791
- });
792
- });
793
- }, PRIME_POST_FIRE_REFRESH_MS);
794
- this.postFireRefreshTimers.add(timer);
795
- if (typeof timer === 'object' && 'unref' in timer) {
796
- ;
797
- timer.unref?.();
798
- }
799
- }
800
- async sweepMarkers(now) {
801
- const dir = this.storageMarkerDir();
802
- const swept = await this.sweepMarkerDirectory(dir, now);
803
- if (swept > 0) {
804
- logger.trace('prime', 'swept stale markers', { swept });
805
- }
806
- }
807
- async sweepMarkerDirectory(dir, now) {
808
- let entries;
809
- try {
810
- entries = await readdir(dir);
811
- }
812
- catch {
813
- return 0;
814
- }
815
- let swept = 0;
816
- for (const entry of entries) {
817
- try {
818
- const path = join(dir, entry);
819
- const s = await stat(path);
820
- if (s.isDirectory()) {
821
- swept += await this.sweepMarkerDirectory(path, now);
822
- continue;
823
- }
824
- if (now - s.mtimeMs > PRIME_MARKER_MAX_AGE_MS) {
825
- await rm(path, { force: true });
826
- swept += 1;
827
- }
828
- }
829
- catch {
830
- // Markers can vanish between readdir and stat; skip silently.
831
- }
832
- }
833
- return swept;
834
- }
835
- storageMarkerDir() {
836
- return primeMarkerNamespaceDir(this.options.markerDir ?? defaultMarkerDir(), this.options.storagePath);
837
- }
838
- markerDir(accountFingerprint) {
839
- return primeAccountMarkerDir(this.options.markerDir ?? defaultMarkerDir(), this.options.storagePath, accountFingerprint);
840
- }
841
- }