@crouter/api 0.3.387 → 0.3.388

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 (130) hide show
  1. package/dist/api/__tests__/integration/client.test.js +97 -0
  2. package/dist/api/client.d.ts +7 -0
  3. package/dist/api/client.js +40 -21
  4. package/dist/core/asset-root.d.ts +7 -0
  5. package/dist/core/asset-root.js +18 -0
  6. package/dist/core/canvas/boot-id.d.ts +6 -0
  7. package/dist/core/canvas/boot-id.js +26 -0
  8. package/dist/core/canvas/paths.d.ts +72 -0
  9. package/dist/core/canvas/paths.js +163 -0
  10. package/dist/core/canvas/pid.d.ts +391 -0
  11. package/dist/core/canvas/pid.js +948 -0
  12. package/dist/core/command-plugins/bundle.d.ts +149 -0
  13. package/dist/core/command-plugins/bundle.js +588 -0
  14. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  15. package/dist/core/command-plugins/endpoint.js +51 -0
  16. package/dist/core/config.d.ts +233 -0
  17. package/dist/core/config.js +1120 -0
  18. package/dist/core/env-name.d.ts +6 -0
  19. package/dist/core/env-name.js +9 -0
  20. package/dist/core/errors.d.ts +38 -0
  21. package/dist/core/errors.js +90 -0
  22. package/dist/core/events/emit.d.ts +6 -0
  23. package/dist/core/events/emit.js +42 -0
  24. package/dist/core/events/envelope.d.ts +2 -0
  25. package/dist/core/events/envelope.js +84 -0
  26. package/dist/core/events/errors.d.ts +4 -0
  27. package/dist/core/events/errors.js +69 -0
  28. package/dist/core/events/operation-id.d.ts +4 -0
  29. package/dist/core/events/operation-id.js +24 -0
  30. package/dist/core/events/serialize.d.ts +4 -0
  31. package/dist/core/events/serialize.js +199 -0
  32. package/dist/core/events/source.d.ts +16 -0
  33. package/dist/core/events/source.js +31 -0
  34. package/dist/core/events/types.d.ts +68 -0
  35. package/dist/core/events/types.js +11 -0
  36. package/dist/core/exclusive-lock.d.ts +34 -0
  37. package/dist/core/exclusive-lock.js +197 -0
  38. package/dist/core/fs-utils.d.ts +44 -0
  39. package/dist/core/fs-utils.js +208 -0
  40. package/dist/core/help.d.ts +309 -0
  41. package/dist/core/help.js +406 -0
  42. package/dist/core/human/page-catalog.d.ts +57 -0
  43. package/dist/core/human/page-catalog.js +172 -0
  44. package/dist/core/installed-plugins.d.ts +2 -0
  45. package/dist/core/installed-plugins.js +79 -0
  46. package/dist/core/io.d.ts +122 -0
  47. package/dist/core/io.js +373 -0
  48. package/dist/core/keybindings/attach-control.d.ts +49 -0
  49. package/dist/core/keybindings/attach-control.js +42 -0
  50. package/dist/core/keybindings/catalog.d.ts +18 -0
  51. package/dist/core/keybindings/catalog.js +257 -0
  52. package/dist/core/keybindings/types.d.ts +42 -0
  53. package/dist/core/keybindings/types.js +1 -0
  54. package/dist/core/layout.d.ts +26 -0
  55. package/dist/core/layout.js +94 -0
  56. package/dist/core/locked-file.d.ts +27 -0
  57. package/dist/core/locked-file.js +118 -0
  58. package/dist/core/log.d.ts +9 -0
  59. package/dist/core/log.js +89 -0
  60. package/dist/core/manifest.d.ts +5 -0
  61. package/dist/core/manifest.js +15 -0
  62. package/dist/core/plugin-env.d.ts +8 -0
  63. package/dist/core/plugin-env.js +31 -0
  64. package/dist/core/plugin-extensions.d.ts +29 -0
  65. package/dist/core/plugin-extensions.js +191 -0
  66. package/dist/core/plugin-swap-lock.d.ts +9 -0
  67. package/dist/core/plugin-swap-lock.js +31 -0
  68. package/dist/core/preview-result-path.d.ts +4 -0
  69. package/dist/core/preview-result-path.js +26 -0
  70. package/dist/core/profiles/env-store.d.ts +22 -0
  71. package/dist/core/profiles/env-store.js +163 -0
  72. package/dist/core/profiles/fuzzy-match.d.ts +19 -0
  73. package/dist/core/profiles/fuzzy-match.js +92 -0
  74. package/dist/core/profiles/manifest.d.ts +120 -0
  75. package/dist/core/profiles/manifest.js +529 -0
  76. package/dist/core/rate-limit-scope.d.ts +25 -0
  77. package/dist/core/rate-limit-scope.js +64 -0
  78. package/dist/core/render.d.ts +12 -0
  79. package/dist/core/render.js +138 -0
  80. package/dist/core/resolver.d.ts +14 -0
  81. package/dist/core/resolver.js +111 -0
  82. package/dist/core/runtime/branded-host.d.ts +25 -0
  83. package/dist/core/runtime/branded-host.js +264 -0
  84. package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
  85. package/dist/core/runtime/broker/daemon-ops.js +177 -0
  86. package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
  87. package/dist/core/runtime/broker/signal-stream.js +149 -0
  88. package/dist/core/scope.d.ts +32 -0
  89. package/dist/core/scope.js +184 -0
  90. package/dist/core/scoped-state/db.d.ts +17 -0
  91. package/dist/core/scoped-state/db.js +247 -0
  92. package/dist/core/scoped-state/migrate.d.ts +8 -0
  93. package/dist/core/scoped-state/migrate.js +187 -0
  94. package/dist/core/scoped-state/paths.d.ts +9 -0
  95. package/dist/core/scoped-state/paths.js +27 -0
  96. package/dist/core/scoped-state/profiles.d.ts +27 -0
  97. package/dist/core/scoped-state/profiles.js +93 -0
  98. package/dist/core/scoped-state/providers.d.ts +24 -0
  99. package/dist/core/scoped-state/providers.js +19 -0
  100. package/dist/core/scoped-state/schema.d.ts +6 -0
  101. package/dist/core/scoped-state/schema.js +43 -0
  102. package/dist/core/scoped-state/settings.d.ts +28 -0
  103. package/dist/core/scoped-state/settings.js +83 -0
  104. package/dist/core/spaces/open-beneath.d.ts +71 -0
  105. package/dist/core/spaces/open-beneath.js +581 -0
  106. package/dist/core/sqlite-statements.d.ts +4 -0
  107. package/dist/core/sqlite-statements.js +17 -0
  108. package/dist/core/subscription-state.d.ts +121 -0
  109. package/dist/core/subscription-state.js +287 -0
  110. package/dist/core/user-settings.d.ts +377 -0
  111. package/dist/core/user-settings.js +458 -0
  112. package/dist/daemon/broker-signals/bus.d.ts +30 -0
  113. package/dist/daemon/broker-signals/bus.js +87 -0
  114. package/dist/daemon/manage.d.ts +176 -0
  115. package/dist/daemon/manage.js +664 -0
  116. package/dist/daemon/pidfile.d.ts +8 -0
  117. package/dist/daemon/pidfile.js +37 -0
  118. package/dist/daemon/startup-policy.d.ts +1 -0
  119. package/dist/daemon/startup-policy.js +1 -0
  120. package/dist/native/linux.d.ts +29 -0
  121. package/dist/native/linux.js +20 -0
  122. package/dist/shared/env.d.ts +116 -0
  123. package/dist/shared/env.js +271 -0
  124. package/dist/shared/inbox-entry-body.d.ts +22 -0
  125. package/dist/shared/inbox-entry-body.js +116 -0
  126. package/dist/shared/working-activity.d.ts +9 -0
  127. package/dist/shared/working-activity.js +27 -0
  128. package/dist/types.d.ts +562 -0
  129. package/dist/types.js +186 -0
  130. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ import { type RateLimitScope } from './rate-limit-scope.js';
2
+ export declare const ANTHROPIC_PROVIDER_ID = "anthropic";
3
+ export declare const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
4
+ /** pi's built-in `openai` provider signed in with a ChatGPT account (dynamic OAuth client). Distinct from Codex: its own pool, credential file and sign-in. */
5
+ export declare const OPENAI_CHATGPT_PROVIDER_ID = "openai";
6
+ export declare const INVALID_REFRESH_TOKEN_BACKOFF_MS: number;
7
+ export type ManagedProviderId = typeof ANTHROPIC_PROVIDER_ID | typeof OPENAI_CODEX_PROVIDER_ID | typeof OPENAI_CHATGPT_PROVIDER_ID;
8
+ export declare const MANAGED_PROVIDER_IDS: readonly ManagedProviderId[];
9
+ export type ModelStrength = 'ultra' | 'strong' | 'medium' | 'light';
10
+ export type RotationConfig = {
11
+ defaultFallbackStrength?: ModelStrength;
12
+ revertWhenAvailable?: boolean;
13
+ };
14
+ /** `rateLimitedUntil` cools the whole account; `modelCooldowns` cools one model family on it (family → epoch ms, stored rows including expired ones). Read both through `coolingUntilFor`. */
15
+ export type SubscriptionCredential = {
16
+ label: string;
17
+ refresh: string;
18
+ access: string;
19
+ expires: number;
20
+ rateLimitedUntil: number;
21
+ lastAttemptAt: number;
22
+ lastRateLimitedAt: number;
23
+ authFailure?: 'invalid_grant';
24
+ accountId?: string;
25
+ clientId?: string;
26
+ modelCooldowns?: Record<string, number>;
27
+ };
28
+ export type ProviderCandidate = {
29
+ providerId: string;
30
+ modelId: string;
31
+ thinkingLevel?: string;
32
+ registered: boolean;
33
+ authenticated: boolean;
34
+ coolingUntil?: number;
35
+ automaticFallback?: boolean;
36
+ routeId?: string;
37
+ credentialSource?: string;
38
+ family?: string;
39
+ strength?: ModelStrength;
40
+ rank?: number;
41
+ };
42
+ export type ProviderCandidateRejection = 'unregistered' | 'non-managed-fallback' | 'unauthenticated' | 'cooling';
43
+ /** Refreshed tokens; `clientId` is set when the vendor rotated the issued OAuth client id. */
44
+ export type RefreshedTokens = {
45
+ refresh: string;
46
+ access: string;
47
+ expires: number;
48
+ clientId?: string;
49
+ };
50
+ /** When this account can next serve `modelId`: the later of its account-wide cooldown and the cooldown on that model's family. Without a model only the account-wide cooldown applies. 0 or a past time means ready. */
51
+ export declare function coolingUntilFor(entry: Pick<SubscriptionCredential, 'rateLimitedUntil' | 'modelCooldowns'>, modelId?: string): number;
52
+ /** Every cooldown on an account still in force at `now`: `scope` is `account` or the model family. */
53
+ export declare function activeCooldowns(entry: Pick<SubscriptionCredential, 'rateLimitedUntil' | 'modelCooldowns'>, now?: number): Array<{
54
+ scope: string;
55
+ until: number;
56
+ }>;
57
+ /** Human text for an account's active cooldowns (`cooldown 12m`, `opus cooldown 3d 4h`), or undefined when it is ready. */
58
+ export declare function formatCooldowns(entry: Pick<SubscriptionCredential, 'rateLimitedUntil' | 'modelCooldowns'>, now?: number): string | undefined;
59
+ /** Whether a successful call on `modelId` has stored cooldown or auth health to clear; callers skip the write otherwise (every provider write bumps provider_revision). */
60
+ export declare function subscriptionNeedsSuccessWrite(entry: Pick<SubscriptionCredential, 'rateLimitedUntil' | 'authFailure' | 'modelCooldowns'>, modelId?: string): boolean;
61
+ export declare function isManagedProvider(providerId: string | undefined): providerId is ManagedProviderId;
62
+ /** Stable per-install UUID sent to OpenAI as `ext_agent_host_id=urn:uuid:<id>` during ChatGPT sign-in. The install id is not a UUID, so this is its own get-or-create secret. */
63
+ export declare function openAIDeviceId(): string;
64
+ export declare function getProviderLabel(providerId: ManagedProviderId): string;
65
+ export declare function resolveProviderCandidates(candidates: readonly ProviderCandidate[], now?: number, options?: {
66
+ pinnedToFirst?: boolean;
67
+ }): {
68
+ candidates: ProviderCandidate[];
69
+ rejected: Array<{
70
+ candidate: ProviderCandidate;
71
+ reason: ProviderCandidateRejection;
72
+ }>;
73
+ };
74
+ export declare class DuplicateSubscriptionError extends Error {
75
+ constructor(message: string);
76
+ }
77
+ export declare function readSubscriptionPool(provider: ManagedProviderId): SubscriptionCredential[];
78
+ export declare function writeSubscriptionPool(provider: ManagedProviderId, next: SubscriptionCredential[]): Promise<SubscriptionCredential[]>;
79
+ export declare function addSubscription(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
80
+ export declare function upsertSubscription(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
81
+ export declare function upsertSubscriptionWithUniqueAccount(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
82
+ export declare function renameSubscription(provider: ManagedProviderId, ref: string, nextLabel: string): Promise<SubscriptionCredential[]>;
83
+ export declare function removeManagedAccount(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
84
+ export declare function removeSubscription(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
85
+ export declare function removeAllManagedAccounts(provider: ManagedProviderId): Promise<number>;
86
+ export declare function removeManagedProvider(provider: ManagedProviderId, label?: string): Promise<SubscriptionCredential[]>;
87
+ export declare function promoteSubscription(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
88
+ export declare function markSubscriptionAttempt(provider: ManagedProviderId, ref: string, attemptAt?: number): Promise<SubscriptionCredential[]>;
89
+ export declare function markSubscriptionAuthenticationFailed(provider: ManagedProviderId, ref: string, attempted: Pick<SubscriptionCredential, 'refresh' | 'access'>, retryAfterMs: number, attemptAt?: number): Promise<SubscriptionCredential[]>;
90
+ /** Record a 429 on an account. `scope` defaults to the whole account; a model scope cools only that family on this account, and a 429 older than the newest one already recorded for the same scope is ignored. */
91
+ export declare function markSubscriptionRateLimited(provider: ManagedProviderId, ref: string, retryAfterMs: number, attemptAt?: number, rateLimitedAt?: number, scope?: RateLimitScope): Promise<SubscriptionCredential[]>;
92
+ /** A call on `modelId` succeeded: clear the account-wide cooldown and auth failure, and that model family's cooldown, unless a newer 429 for the same scope landed after `attemptAt` began. */
93
+ export declare function markSubscriptionSuccess(provider: ManagedProviderId, ref: string, attemptAt?: number, modelId?: string): Promise<SubscriptionCredential[]>;
94
+ export declare function refreshSubscriptionCredential(provider: ManagedProviderId, label: string, exchange: (refreshToken: string, clientId?: string) => Promise<RefreshedTokens>): Promise<SubscriptionCredential>;
95
+ export declare function isInvalidRefreshTokenError(error: unknown): boolean;
96
+ export declare function modifyElectedSubscription(provider: ManagedProviderId, elect: (pool: SubscriptionCredential[]) => SubscriptionCredential | undefined, apply: (current: SubscriptionCredential) => Promise<RefreshedTokens | undefined>): Promise<SubscriptionCredential | undefined>;
97
+ export type ManagedLoginIdentity = {
98
+ accountId?: string;
99
+ email?: string;
100
+ };
101
+ export declare function enrichManagedLoginIdentity(provider: ManagedProviderId, value: {
102
+ access: string;
103
+ accountId?: string;
104
+ email?: string;
105
+ }): Promise<ManagedLoginIdentity>;
106
+ export declare function commitManagedLogin(provider: ManagedProviderId, label: string | undefined, value: {
107
+ refresh: string;
108
+ access: string;
109
+ expires: number;
110
+ accountId?: string;
111
+ clientId?: string;
112
+ email?: string;
113
+ lastRateLimitedAt: number;
114
+ autoLabel?: boolean;
115
+ }): Promise<SubscriptionCredential[]>;
116
+ export declare function parseRetryAfterHeader(value: string | undefined, now?: number): number | undefined;
117
+ export declare function retryAfterMsFromHeaders(headers: Headers | Record<string, string> | undefined): number | undefined;
118
+ export declare function retryAfterMsFromError(error: unknown): number | undefined;
119
+ export declare function readRotationConfig(): RotationConfig;
120
+ export declare function writeRotationConfig(next: RotationConfig): RotationConfig;
121
+ export declare function formatStatusLine(provider: ManagedProviderId, now?: number, modelId?: string): string;
@@ -0,0 +1,287 @@
1
+ import { prepared as cachedStatement } from './sqlite-statements.js';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { brokerConfigSnapshot } from './config.js';
6
+ import { LockedJsonFile } from './locked-file.js';
7
+ import { secretsDir } from './layout.js';
8
+ import { openScopedStore } from './scoped-state/db.js';
9
+ import { providerStore } from './scoped-state/paths.js';
10
+ import { providerModelCooldowns, providerModelCooldownsFor, providerRowByReference, providerRows, providerWrite } from './scoped-state/providers.js';
11
+ import { modelCooldownFamily } from './rate-limit-scope.js';
12
+ export const ANTHROPIC_PROVIDER_ID = 'anthropic';
13
+ export const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
14
+ /** pi's built-in `openai` provider signed in with a ChatGPT account (dynamic OAuth client). Distinct from Codex: its own pool, credential file and sign-in. */
15
+ export const OPENAI_CHATGPT_PROVIDER_ID = 'openai';
16
+ export const INVALID_REFRESH_TOKEN_BACKOFF_MS = 30 * 24 * 60 * 60 * 1000;
17
+ export const MANAGED_PROVIDER_IDS = [ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, OPENAI_CHATGPT_PROVIDER_ID];
18
+ const DEFAULT_FALLBACK_STRENGTH = 'strong';
19
+ const PROVIDERS = { anthropic: { label: 'Claude', credentials: 'anthropic-credentials.json' }, 'openai-codex': { label: 'OpenAI Codex', credentials: 'openai-codex-credentials.json' }, openai: { label: 'ChatGPT', credentials: 'openai-credentials.json' } };
20
+ function stored(entry) { return { refresh: entry.refresh, access: entry.access, expires: entry.expires, ...(entry.accountId ? { accountId: entry.accountId } : {}), ...(entry.clientId ? { clientId: entry.clientId } : {}) }; }
21
+ function optionalString(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
22
+ function agentDir() { return secretsDir(); }
23
+ function credentialFile(provider) { return join(agentDir(), PROVIDERS[provider].credentials); }
24
+ function normalizeStrength(value) { return value === 'ultra' || value === 'strong' || value === 'medium' || value === 'light' ? value : undefined; }
25
+ function key(label) { return label.toLowerCase(); }
26
+ function credential(input) { const label = typeof input.label === 'string' ? input.label.trim() : ''; const refresh = typeof input.refresh === 'string' ? input.refresh.trim() : ''; const access = typeof input.access === 'string' ? input.access.trim() : ''; const expires = Number(input.expires); if (!label || !refresh || !access || !Number.isFinite(expires))
27
+ return undefined; const accountId = optionalString(input.accountId); const clientId = optionalString(input.clientId); return { label, refresh, access, expires, rateLimitedUntil: Number.isFinite(Number(input.rateLimitedUntil)) ? Number(input.rateLimitedUntil) : 0, lastAttemptAt: Number.isFinite(Number(input.lastAttemptAt)) ? Number(input.lastAttemptAt) : 0, lastRateLimitedAt: Number.isFinite(Number(input.lastRateLimitedAt)) ? Number(input.lastRateLimitedAt) : 0, ...(input.authFailure === 'invalid_grant' ? { authFailure: 'invalid_grant' } : {}), ...(accountId ? { accountId } : {}), ...(clientId ? { clientId } : {}), ...(modelCooldowns(input.modelCooldowns) ? { modelCooldowns: modelCooldowns(input.modelCooldowns) } : {}) }; }
28
+ function modelCooldowns(value) { if (!value || typeof value !== 'object' || Array.isArray(value))
29
+ return undefined; const entries = Object.entries(value).filter(([family, until]) => family.trim() !== '' && Number.isFinite(Number(until))).map(([family, until]) => [family, Number(until)]); return entries.length ? Object.fromEntries(entries) : undefined; }
30
+ function assertCredential(provider, entry) { if (!entry.label?.trim())
31
+ throw new Error(`${getProviderLabel(provider)} subscription label cannot be empty`); const result = credential(entry); if (!result)
32
+ throw new Error(`Refusing to persist incomplete ${getProviderLabel(provider)} subscription "${entry.label.trim()}"; pool rows require both OAuth tokens.`); return result; }
33
+ function parseCredentials(path, current) { if (!current || current.trim() === '')
34
+ return { version: 1, accounts: {} }; let parsed; try {
35
+ parsed = JSON.parse(current);
36
+ }
37
+ catch (error) {
38
+ throw new Error(`managed credential file ${path} is corrupt: ${error instanceof Error ? error.message : String(error)}`);
39
+ } if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) || parsed.version !== 1 || typeof parsed.accounts !== 'object' || parsed.accounts === null || Array.isArray(parsed.accounts))
40
+ throw new Error(`managed credential file ${path} is corrupt`); return parsed; }
41
+ function toPublic(row, file, cooldowns = []) { const stored = file.accounts[row.id]; if (!stored || typeof stored.refresh !== 'string' || typeof stored.access !== 'string' || !Number.isFinite(stored.expires))
42
+ throw new Error(`managed ${row.providerId} account ${row.id} has no credential record`); return { label: row.label, refresh: stored.refresh, access: stored.access, expires: stored.expires, rateLimitedUntil: row.rateLimitedUntil, lastAttemptAt: row.lastAttemptAt, lastRateLimitedAt: row.lastRateLimitedAt, ...(row.authFailure ? { authFailure: row.authFailure } : {}), ...(stored.accountId ? { accountId: stored.accountId } : {}), ...(stored.clientId ? { clientId: stored.clientId } : {}), ...(cooldowns.length ? { modelCooldowns: Object.fromEntries(cooldowns.map((cooldown) => [cooldown.family, cooldown.rateLimitedUntil])) } : {}) }; }
43
+ function rowsWithCredentials(provider, file) { const cooldowns = providerModelCooldownsFor(agentDir(), provider); return providerRows(agentDir(), provider).map((row) => ({ row, entry: toPublic(row, file, cooldowns.get(row.id)) })); }
44
+ function managedCredentialFile(provider) { return new LockedJsonFile(credentialFile(provider), true, '{"version":1,"accounts":{}}\n'); }
45
+ function readFile(provider) { return parseCredentials(credentialFile(provider), managedCredentialFile(provider).readUnlocked()); }
46
+ /** A slot re-signed into a provably different account (both account ids known and unequal): its model-family cooldowns were the old account's server quota windows, not this one's. Unknown or matching ids keep them. */
47
+ function differentAccount(previous, next) { const a = previous.accountId?.trim(); const b = next.accountId?.trim(); return Boolean(a && b && a !== b); }
48
+ function identity(entry) { return entry.accountId?.trim() || (entry.refresh.trim() ? `refresh:${createHash('sha256').update(entry.refresh.trim()).digest('hex')}` : undefined); }
49
+ function withClientId(tokens, clientId) { const id = optionalString(clientId); return id ? { ...tokens, clientId: id } : tokens; }
50
+ function assertTokens(provider, label, value) { const refresh = value.refresh?.trim() ?? ''; const access = value.access?.trim() ?? ''; const expires = Number(value.expires); if (!refresh || !access || !Number.isFinite(expires))
51
+ throw new Error(`${getProviderLabel(provider)} subscription "${label}" returned an incomplete credential; the stored credential was left unchanged.`); return { refresh, access, expires }; }
52
+ /** When this account can next serve `modelId`: the later of its account-wide cooldown and the cooldown on that model's family. Without a model only the account-wide cooldown applies. 0 or a past time means ready. */
53
+ export function coolingUntilFor(entry, modelId) { const family = modelCooldownFamily(modelId); return Math.max(entry.rateLimitedUntil || 0, family === undefined ? 0 : entry.modelCooldowns?.[family] ?? 0); }
54
+ /** Every cooldown on an account still in force at `now`: `scope` is `account` or the model family. */
55
+ export function activeCooldowns(entry, now = Date.now()) { return [...((entry.rateLimitedUntil || 0) > now ? [{ scope: 'account', until: entry.rateLimitedUntil }] : []), ...Object.entries(entry.modelCooldowns ?? {}).filter(([, until]) => until > now).map(([scope, until]) => ({ scope, until }))]; }
56
+ /** Human text for an account's active cooldowns (`cooldown 12m`, `opus cooldown 3d 4h`), or undefined when it is ready. */
57
+ export function formatCooldowns(entry, now = Date.now()) { const parts = activeCooldowns(entry, now).map(({ scope, until }) => `${scope === 'account' ? '' : `${scope} `}cooldown ${formatWait(until - now)}`); return parts.length ? parts.join(', ') : undefined; }
58
+ function formatWait(ms) { const minutes = Math.max(1, Math.ceil(ms / 60_000)); if (minutes < 60)
59
+ return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24)
60
+ return `${hours}h${minutes % 60 ? ` ${minutes % 60}m` : ''}`; return `${Math.floor(hours / 24)}d${hours % 24 ? ` ${hours % 24}h` : ''}`; }
61
+ function hasModelCooldownRow(entry, modelId) { const family = modelCooldownFamily(modelId); return family !== undefined && entry.modelCooldowns?.[family] !== undefined; }
62
+ /** Whether a successful call on `modelId` has stored cooldown or auth health to clear; callers skip the write otherwise (every provider write bumps provider_revision). */
63
+ export function subscriptionNeedsSuccessWrite(entry, modelId) { return Boolean(entry.rateLimitedUntil || entry.authFailure) || hasModelCooldownRow(entry, modelId); }
64
+ export function isManagedProvider(providerId) { return providerId === ANTHROPIC_PROVIDER_ID || providerId === OPENAI_CODEX_PROVIDER_ID || providerId === OPENAI_CHATGPT_PROVIDER_ID; }
65
+ /** Stable per-install UUID sent to OpenAI as `ext_agent_host_id=urn:uuid:<id>` during ChatGPT sign-in. The install id is not a UUID, so this is its own get-or-create secret. */
66
+ export function openAIDeviceId() { const path = join(agentDir(), 'openai-device-id'); try {
67
+ const existing = readFileSync(path, 'utf8').trim();
68
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(existing))
69
+ return existing.toLowerCase();
70
+ }
71
+ catch { /* create below */ } const id = randomUUID(); mkdirSync(agentDir(), { recursive: true, mode: 0o700 }); writeFileSync(path, `${id}\n`, { mode: 0o600 }); return id; }
72
+ export function getProviderLabel(providerId) { return PROVIDERS[providerId].label; }
73
+ export function resolveProviderCandidates(candidates, now = Date.now(), options = {}) { const viable = []; const rejected = []; for (let i = 0; i < candidates.length; i++) {
74
+ const candidate = candidates[i];
75
+ const reason = !candidate.registered ? 'unregistered' : candidate.automaticFallback === false ? 'non-managed-fallback' : !candidate.authenticated ? 'unauthenticated' : (candidate.coolingUntil ?? 0) > now ? 'cooling' : undefined;
76
+ if (!reason)
77
+ viable.push(candidate);
78
+ else {
79
+ rejected.push({ candidate, reason });
80
+ if (options.pinnedToFirst && i === 0 && reason !== 'cooling')
81
+ break;
82
+ }
83
+ } return { candidates: viable, rejected }; }
84
+ export class DuplicateSubscriptionError extends Error {
85
+ constructor(message) { super(message); this.name = 'DuplicateSubscriptionError'; }
86
+ }
87
+ export function readSubscriptionPool(provider) { return rowsWithCredentials(provider, readFile(provider)).map(({ entry }) => entry); }
88
+ async function locked(provider, run) { const path = credentialFile(provider); return managedCredentialFile(provider).withLockAsync(async (current, _held, publish) => { const file = parseCredentials(path, current); return { result: await run(file, rowsWithCredentials(provider, file), (next) => publish(`${JSON.stringify(next)}\n`)) }; }); }
89
+ function insert(db, provider, row) { cachedStatement(db, 'INSERT INTO managed_accounts(provider_id,id,label,label_key,position,last_attempt_at,last_rate_limited_at,rate_limited_until,auth_failure) VALUES(?,?,?,?,?,?,?,?,?)').run(provider, row.id, row.label, row.labelKey, row.position, row.lastAttemptAt, row.lastRateLimitedAt, row.rateLimitedUntil, row.authFailure ?? null); }
90
+ function update(db, row) { cachedStatement(db, 'UPDATE managed_accounts SET label=?,label_key=?,position=?,last_attempt_at=?,last_rate_limited_at=?,rate_limited_until=?,auth_failure=? WHERE provider_id=? AND id=?').run(row.label, row.labelKey, row.position, row.lastAttemptAt, row.lastRateLimitedAt, row.rateLimitedUntil, row.authFailure ?? null, row.providerId, row.id); }
91
+ function upsertModelCooldown(db, provider, accountId, family, until, rateLimitedAt) { cachedStatement(db, 'INSERT INTO managed_account_model_cooldowns(provider_id,account_id,family,rate_limited_until,last_rate_limited_at) VALUES(?,?,?,?,?) ON CONFLICT(provider_id,account_id,family) DO UPDATE SET rate_limited_until=excluded.rate_limited_until,last_rate_limited_at=excluded.last_rate_limited_at').run(provider, accountId, family, until, rateLimitedAt); }
92
+ function deleteModelCooldowns(db, provider, accountId) { if (accountId === undefined)
93
+ cachedStatement(db, 'DELETE FROM managed_account_model_cooldowns WHERE provider_id=?').run(provider);
94
+ else
95
+ cachedStatement(db, 'DELETE FROM managed_account_model_cooldowns WHERE provider_id=? AND account_id=?').run(provider, accountId); }
96
+ function publicPool(provider, file) { return rowsWithCredentials(provider, file).map(({ entry }) => entry); }
97
+ function writeMembershipAfterCredentialPublish(provider, write) { try {
98
+ write();
99
+ }
100
+ catch (error) {
101
+ throw new Error(`managed ${getProviderLabel(provider)} credential file was published, but its SQL membership update failed; SQL membership remains authoritative: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
102
+ } }
103
+ export async function writeSubscriptionPool(provider, next) { return locked(provider, async (file, rows, publish) => { const normalized = next.map((entry) => assertCredential(provider, entry)); const ids = normalized.map((entry, index) => ({ entry, id: rows[index]?.row.id ?? randomUUID() })); const accounts = {}; for (const { entry, id } of ids)
104
+ accounts[id] = stored(entry); publish({ version: 1, accounts }); writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => { const priorFamilies = providerModelCooldowns(db, provider); cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=?').run(provider); cachedStatement(db, 'DELETE FROM managed_account_model_cooldowns WHERE provider_id=?').run(provider); ids.forEach(({ entry, id }, position) => { insert(db, provider, { providerId: provider, id, label: entry.label, labelKey: key(entry.label), position, lastAttemptAt: entry.lastAttemptAt, lastRateLimitedAt: entry.lastRateLimitedAt, rateLimitedUntil: entry.rateLimitedUntil, ...(entry.authFailure ? { authFailure: entry.authFailure } : {}) }); for (const [family, until] of Object.entries(entry.modelCooldowns ?? {}))
105
+ upsertModelCooldown(db, provider, id, family, until, priorFamilies.get(id)?.find((prior) => prior.family === family)?.lastRateLimitedAt ?? entry.lastRateLimitedAt); }); })); return normalized; }); }
106
+ function resolve(rows, ref) { const needle = ref.trim().toLowerCase(); const number = Number.parseInt(needle, 10); if (Number.isFinite(number) && String(number) === needle && number >= 1 && number <= rows.length)
107
+ return number - 1; return rows.findIndex(({ entry }) => key(entry.label) === needle); }
108
+ export async function addSubscription(provider, entry) { const checked = assertCredential(provider, entry); return locked(provider, async (file, rows, publish) => { if (rows.some(({ entry: existing }) => key(existing.label) === key(checked.label)))
109
+ throw new DuplicateSubscriptionError(`${getProviderLabel(provider)} subscription already exists: ${checked.label}`); const same = identity(checked); const collision = rows.find(({ entry: existing }) => identity(existing) === same); if (same && collision)
110
+ throw new DuplicateSubscriptionError(`This ${getProviderLabel(provider)} account is already added as “${collision.entry.label}” — it shares the same quota, so a second slot adds no redundancy.`); const id = randomUUID(); file.accounts[id] = stored(checked); publish(file); writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => insert(db, provider, { providerId: provider, id, label: checked.label, labelKey: key(checked.label), position: rows.length, lastAttemptAt: checked.lastAttemptAt, lastRateLimitedAt: checked.lastRateLimitedAt, rateLimitedUntil: checked.rateLimitedUntil, ...(checked.authFailure ? { authFailure: checked.authFailure } : {}) }))); return publicPool(provider, file); }); }
111
+ export async function upsertSubscription(provider, entry) { const checked = assertCredential(provider, entry); const existing = readSubscriptionPool(provider).find((x) => key(x.label) === key(checked.label)); return existing ? upsertSubscriptionWithUniqueAccount(provider, checked) : addSubscription(provider, checked); }
112
+ export async function upsertSubscriptionWithUniqueAccount(provider, entry) {
113
+ const checked = assertCredential(provider, entry);
114
+ return locked(provider, async (file, rows, publish) => {
115
+ const index = rows.findIndex(({ entry: existing }) => key(existing.label) === key(checked.label));
116
+ const existing = rows[index];
117
+ const duplicate = rows.find(({ entry: other }, i) => i !== index && identity(other) === identity(checked));
118
+ if (identity(checked) && duplicate)
119
+ throw new DuplicateSubscriptionError(`This ${getProviderLabel(provider)} account is already added as “${duplicate.entry.label}” — it shares the same quota, so a second slot adds no redundancy.`);
120
+ if (!existing) {
121
+ const id = randomUUID();
122
+ file.accounts[id] = stored(checked);
123
+ publish(file);
124
+ writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => insert(db, provider, { providerId: provider, id, label: checked.label, labelKey: key(checked.label), position: rows.length, lastAttemptAt: checked.lastAttemptAt, lastRateLimitedAt: checked.lastRateLimitedAt, rateLimitedUntil: checked.rateLimitedUntil })));
125
+ return publicPool(provider, file);
126
+ }
127
+ const row = existing.row;
128
+ const cooldownIncoming = checked.lastRateLimitedAt >= row.lastRateLimitedAt;
129
+ file.accounts[row.id] = stored(checked);
130
+ publish(file);
131
+ writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => { update(db, { ...row, label: checked.label, labelKey: key(checked.label), lastAttemptAt: Math.max(row.lastAttemptAt, checked.lastAttemptAt), lastRateLimitedAt: cooldownIncoming ? checked.lastRateLimitedAt : row.lastRateLimitedAt, rateLimitedUntil: cooldownIncoming ? checked.rateLimitedUntil : row.rateLimitedUntil }); if (differentAccount(existing.entry, checked))
132
+ deleteModelCooldowns(db, provider, row.id); }));
133
+ return publicPool(provider, file);
134
+ });
135
+ }
136
+ export async function renameSubscription(provider, ref, nextLabel) { return locked(provider, async (file, rows) => { const index = resolve(rows, ref); if (index < 0)
137
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const label = nextLabel.trim(); if (!label)
138
+ throw new Error(`${getProviderLabel(provider)} subscription label cannot be empty`); if (rows.some((value, i) => i !== index && key(value.entry.label) === key(label)))
139
+ throw new DuplicateSubscriptionError(`${getProviderLabel(provider)} subscription already exists: ${label}`); providerWrite(agentDir(), (db) => update(db, { ...rows[index].row, label, labelKey: key(label) })); return publicPool(provider, file); }); }
140
+ async function remove(provider, ref) { return locked(provider, async (file, rows, publish) => { const index = resolve(rows, ref); if (index < 0)
141
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const removed = rows[index].row; providerWrite(agentDir(), (db) => { cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=? AND id=?').run(provider, removed.id); deleteModelCooldowns(db, provider, removed.id); }); delete file.accounts[removed.id]; publish(file); return publicPool(provider, file); }); }
142
+ export async function removeManagedAccount(provider, ref) { return remove(provider, ref); }
143
+ export async function removeSubscription(provider, ref) { return remove(provider, ref); }
144
+ export async function removeAllManagedAccounts(provider) { return locked(provider, async (file, rows, publish) => { providerWrite(agentDir(), (db) => { cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=?').run(provider); deleteModelCooldowns(db, provider); }); for (const { row } of rows)
145
+ delete file.accounts[row.id]; publish(file); return rows.length; }); }
146
+ export async function removeManagedProvider(provider, label) { if (label === undefined) {
147
+ await removeAllManagedAccounts(provider);
148
+ return [];
149
+ } return remove(provider, label); }
150
+ export async function promoteSubscription(provider, ref) { return locked(provider, async (file, rows) => { const index = resolve(rows, ref); if (index < 0)
151
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); providerWrite(agentDir(), (db) => { const reordered = [rows[index], ...rows.filter((_, i) => i !== index)]; reordered.forEach((item, position) => update(db, { ...item.row, position })); }); return publicPool(provider, file); }); }
152
+ async function mutateHealth(provider, ref, mutate) { providerWrite(agentDir(), (db) => { const row = providerRowByReference(db, provider, ref); if (!row)
153
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const next = mutate(row); if (next)
154
+ update(db, next); }); return readSubscriptionPool(provider); }
155
+ export async function markSubscriptionAttempt(provider, ref, attemptAt = Date.now()) { return mutateHealth(provider, ref, (row) => ({ ...row, lastAttemptAt: Math.max(row.lastAttemptAt, attemptAt) })); }
156
+ export async function markSubscriptionAuthenticationFailed(provider, ref, attempted, retryAfterMs, attemptAt = Date.now()) { return locked(provider, async (file, rows) => { const current = rows.find(({ entry }) => key(entry.label) === key(ref)); if (!current)
157
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); if (current.entry.refresh !== attempted.refresh || current.entry.access !== attempted.access)
158
+ return publicPool(provider, file); providerWrite(agentDir(), (db) => update(db, { ...current.row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(current.row.lastAttemptAt, attemptAt), rateLimitedUntil: Math.max(current.row.rateLimitedUntil, attemptAt + Math.max(0, retryAfterMs)) })); return publicPool(provider, file); }); }
159
+ /** Record a 429 on an account. `scope` defaults to the whole account; a model scope cools only that family on this account, and a 429 older than the newest one already recorded for the same scope is ignored. */
160
+ export async function markSubscriptionRateLimited(provider, ref, retryAfterMs, attemptAt = Date.now(), rateLimitedAt = Date.now(), scope = { kind: 'account' }) {
161
+ if (scope.kind === 'account')
162
+ return mutateHealth(provider, ref, (row) => rateLimitedAt < row.lastRateLimitedAt ? null : { ...row, lastAttemptAt: Math.max(row.lastAttemptAt, attemptAt), lastRateLimitedAt: rateLimitedAt, rateLimitedUntil: rateLimitedAt + Math.max(0, retryAfterMs) });
163
+ providerWrite(agentDir(), (db) => {
164
+ const row = providerRowByReference(db, provider, ref);
165
+ if (!row)
166
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`);
167
+ update(db, { ...row, lastAttemptAt: Math.max(row.lastAttemptAt, attemptAt) });
168
+ const current = providerModelCooldowns(db, provider).get(row.id)?.find((cooldown) => cooldown.family === scope.family);
169
+ if (current && rateLimitedAt < current.lastRateLimitedAt)
170
+ return;
171
+ upsertModelCooldown(db, provider, row.id, scope.family, rateLimitedAt + Math.max(0, retryAfterMs), rateLimitedAt);
172
+ });
173
+ return readSubscriptionPool(provider);
174
+ }
175
+ /** A call on `modelId` succeeded: clear the account-wide cooldown and auth failure, and that model family's cooldown, unless a newer 429 for the same scope landed after `attemptAt` began. */
176
+ export async function markSubscriptionSuccess(provider, ref, attemptAt = Date.now(), modelId) {
177
+ const family = modelCooldownFamily(modelId);
178
+ providerWrite(agentDir(), (db) => {
179
+ const row = providerRowByReference(db, provider, ref);
180
+ if (!row)
181
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`);
182
+ if (attemptAt >= row.lastRateLimitedAt)
183
+ update(db, { ...row, authFailure: undefined, lastAttemptAt: Math.max(row.lastAttemptAt, attemptAt), rateLimitedUntil: 0 });
184
+ if (family === undefined)
185
+ return;
186
+ const current = providerModelCooldowns(db, provider).get(row.id)?.find((cooldown) => cooldown.family === family);
187
+ if (current && attemptAt >= current.lastRateLimitedAt)
188
+ cachedStatement(db, 'DELETE FROM managed_account_model_cooldowns WHERE provider_id=? AND account_id=? AND family=?').run(provider, row.id, family);
189
+ });
190
+ return readSubscriptionPool(provider);
191
+ }
192
+ export async function refreshSubscriptionCredential(provider, label, exchange) { return locked(provider, async (file, rows, publish) => { const index = resolve(rows, label); if (index < 0)
193
+ throw new Error(`${getProviderLabel(provider)} subscription "${label}" no longer exists`); const current = rows[index]; if (current.entry.expires > Date.now())
194
+ return current.entry; const next = await exchange(current.entry.refresh, current.entry.clientId); const tokens = withClientId(assertTokens(provider, current.entry.label, next), next.clientId ?? current.entry.clientId); file.accounts[current.row.id] = { ...file.accounts[current.row.id], ...tokens }; publish(file); return { ...current.entry, ...tokens }; }); }
195
+ export function isInvalidRefreshTokenError(error) { const value = error; const message = [value?.errorMessage, value?.message, error instanceof Error ? error.message : ''].filter((part) => typeof part === 'string').join(' '); const status = typeof value?.status === 'number' ? value.status : Number(message.match(/\bstatus[=:]\s*(\d+)/i)?.[1]); return status === 400 && /invalid_grant|refresh token .*invalid|refresh token .*not found/i.test(message); }
196
+ export async function modifyElectedSubscription(provider, elect, apply) { return locked(provider, async (file, rows, publish) => { const attempted = new Set(); let lastError; while (attempted.size < rows.length) {
197
+ const chosen = elect(rows.filter(({ entry }) => !attempted.has(entry.label)).map(({ entry }) => entry));
198
+ if (!chosen)
199
+ break;
200
+ const current = rows.find(({ entry }) => entry.label === chosen.label);
201
+ attempted.add(current.entry.label);
202
+ try {
203
+ const next = await apply(current.entry);
204
+ if (next) {
205
+ const tokens = withClientId(assertTokens(provider, current.entry.label, next), next.clientId ?? current.entry.clientId);
206
+ file.accounts[current.row.id] = { ...file.accounts[current.row.id], ...tokens };
207
+ publish(file);
208
+ return { ...current.entry, ...tokens };
209
+ }
210
+ return current.entry;
211
+ }
212
+ catch (error) {
213
+ if (!isInvalidRefreshTokenError(error))
214
+ throw error;
215
+ lastError = error;
216
+ const at = Date.now();
217
+ providerWrite(agentDir(), (db) => update(db, { ...current.row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(current.row.lastAttemptAt, at), rateLimitedUntil: Math.max(current.row.rateLimitedUntil, at + INVALID_REFRESH_TOKEN_BACKOFF_MS) }));
218
+ }
219
+ } if (lastError)
220
+ throw lastError; return undefined; }); }
221
+ export async function enrichManagedLoginIdentity(provider, value) { const supplied = value.accountId?.trim(); const suppliedEmail = value.email?.trim(); if (provider !== ANTHROPIC_PROVIDER_ID)
222
+ return { ...(supplied ? { accountId: supplied } : {}), ...(suppliedEmail ? { email: suppliedEmail } : {}) }; try {
223
+ const response = await fetch('https://api.anthropic.com/api/oauth/profile', { headers: { Authorization: `Bearer ${value.access}`, Accept: 'application/json' }, signal: AbortSignal.timeout(3_000) });
224
+ if (!response.ok)
225
+ return supplied ? { accountId: supplied } : {};
226
+ const body = await response.json();
227
+ const accountId = typeof body.account?.uuid === 'string' && body.account.uuid.trim() ? body.account.uuid.trim() : undefined;
228
+ const email = typeof body.account?.email === 'string' && body.account.email.trim() ? body.account.email.trim() : undefined;
229
+ return { ...(accountId ? { accountId } : {}), ...(email ? { email } : {}) };
230
+ }
231
+ catch {
232
+ return supplied ? { accountId: supplied } : {};
233
+ } }
234
+ export async function commitManagedLogin(provider, label, value) { const identityInfo = await enrichManagedLoginIdentity(provider, value); return locked(provider, async (file, rows, publish) => { const selected = value.autoLabel ? undefined : label?.trim(); const incomingIdentity = identity({ refresh: value.refresh, accountId: identityInfo.accountId }); const matching = (selected ? rows.find(({ entry }) => key(entry.label) === key(selected)) : undefined) ?? rows.find(({ entry }) => identity(entry) === incomingIdentity); let resolved = matching?.entry.label ?? selected; if (!resolved && value.autoLabel) {
235
+ const base = identityInfo.email || label?.trim() || `Account ${rows.length + 1}`;
236
+ resolved = base;
237
+ for (let n = 2; rows.some(({ entry }) => key(entry.label) === key(resolved)); n++)
238
+ resolved = `${base} (${n})`;
239
+ } if (!resolved)
240
+ throw new Error(`${getProviderLabel(provider)} subscription label cannot be empty`); const entry = assertCredential(provider, { label: resolved, refresh: value.refresh, access: value.access, expires: value.expires, ...(identityInfo.accountId ? { accountId: identityInfo.accountId } : {}), ...(value.clientId ? { clientId: value.clientId } : {}), rateLimitedUntil: 0, lastAttemptAt: Date.now(), lastRateLimitedAt: value.lastRateLimitedAt }); if (!matching) {
241
+ if (rows.some(({ entry: existing }) => key(existing.label) === key(entry.label)))
242
+ throw new DuplicateSubscriptionError(`${getProviderLabel(provider)} subscription already exists: ${entry.label}`);
243
+ const duplicate = rows.find(({ entry: existing }) => identity(existing) === identity(entry));
244
+ if (identity(entry) && duplicate)
245
+ throw new DuplicateSubscriptionError(`This ${getProviderLabel(provider)} account is already added as “${duplicate.entry.label}” — it shares the same quota, so a second slot adds no redundancy.`);
246
+ const id = randomUUID();
247
+ file.accounts[id] = stored(entry);
248
+ publish(file);
249
+ writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => insert(db, provider, { providerId: provider, id, label: entry.label, labelKey: key(entry.label), position: rows.length, lastAttemptAt: entry.lastAttemptAt, lastRateLimitedAt: entry.lastRateLimitedAt, rateLimitedUntil: entry.rateLimitedUntil })));
250
+ return publicPool(provider, file);
251
+ } const row = matching.row; const duplicate = rows.find(({ entry: other }) => other.label !== matching.entry.label && identity(other) === identity(entry)); if (identity(entry) && duplicate)
252
+ throw new DuplicateSubscriptionError(`This ${getProviderLabel(provider)} account is already added as “${duplicate.entry.label}” — it shares the same quota, so a second slot adds no redundancy.`); file.accounts[row.id] = stored(entry); publish(file); const clear = entry.lastRateLimitedAt >= row.lastRateLimitedAt; writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => { update(db, { ...row, authFailure: undefined, lastAttemptAt: Math.max(row.lastAttemptAt, entry.lastAttemptAt), lastRateLimitedAt: clear ? entry.lastRateLimitedAt : row.lastRateLimitedAt, rateLimitedUntil: clear ? 0 : row.rateLimitedUntil }); if (differentAccount(matching.entry, entry))
253
+ deleteModelCooldowns(db, provider, row.id); })); return publicPool(provider, file); }); }
254
+ export function parseRetryAfterHeader(value, now = Date.now()) { if (!value?.trim())
255
+ return undefined; const seconds = Number(value.trim()); if (Number.isFinite(seconds))
256
+ return Math.max(0, Math.round(seconds * 1000)); const parsed = Date.parse(value); return Number.isFinite(parsed) ? Math.max(0, parsed - now) : undefined; }
257
+ export function retryAfterMsFromHeaders(headers) {
258
+ if (!headers)
259
+ return undefined;
260
+ const get = (name) => headers instanceof Headers ? headers.get(name) ?? undefined : Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
261
+ const millis = get('retry-after-ms');
262
+ if (millis !== undefined && millis.trim() !== '' && Number.isFinite(Number(millis)))
263
+ return Math.max(0, Number(millis));
264
+ return parseRetryAfterHeader(get('retry-after'));
265
+ }
266
+ export function retryAfterMsFromError(error) {
267
+ if (!error || typeof error !== 'object' || !('headers' in error))
268
+ return undefined;
269
+ const headers = error.headers;
270
+ if (!(headers instanceof Headers) && (!headers || typeof headers !== 'object' || Array.isArray(headers)))
271
+ return undefined;
272
+ return retryAfterMsFromHeaders(headers);
273
+ }
274
+ export function readRotationConfig() {
275
+ const snapshot = brokerConfigSnapshot();
276
+ if (snapshot)
277
+ return snapshot.rotation;
278
+ const row = cachedStatement(openScopedStore(providerStore(agentDir())), 'SELECT default_fallback_strength,revert_when_available FROM rotation_settings WHERE singleton=1').get();
279
+ return { defaultFallbackStrength: normalizeStrength(row?.default_fallback_strength) ?? DEFAULT_FALLBACK_STRENGTH, revertWhenAvailable: row?.revert_when_available === undefined || row.revert_when_available === null ? true : Boolean(row.revert_when_available) };
280
+ }
281
+ export function writeRotationConfig(next) {
282
+ const current = readRotationConfig();
283
+ const merged = { defaultFallbackStrength: normalizeStrength(next.defaultFallbackStrength) ?? current.defaultFallbackStrength ?? DEFAULT_FALLBACK_STRENGTH, revertWhenAvailable: next.revertWhenAvailable ?? current.revertWhenAvailable ?? true };
284
+ providerWrite(agentDir(), (db) => cachedStatement(db, `INSERT INTO rotation_settings(singleton,default_fallback_strength,revert_when_available) VALUES(1,?,?) ON CONFLICT(singleton) DO UPDATE SET default_fallback_strength=excluded.default_fallback_strength,revert_when_available=excluded.revert_when_available`).run(merged.defaultFallbackStrength, merged.revertWhenAvailable ? 1 : 0));
285
+ return merged;
286
+ }
287
+ export function formatStatusLine(provider, now = Date.now(), modelId) { const pool = readSubscriptionPool(provider); const ready = pool.filter((entry) => entry.authFailure === undefined && coolingUntilFor(entry, modelId) <= now); const label = getProviderLabel(provider); return ready.length === 0 ? `${label} pool exhausted` : `${label} ${ready[0]?.label ?? provider} (${ready.length}/${pool.length} ready)`; }