@centerforagenticai/pi-multi-account 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. package/src/window-history.ts +270 -0
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Public-first discovery: enumerates managed-account credential PRESENCE using
3
+ * Pi's public `readStoredCredential` surface without reading credential values.
4
+ * Pi 0.81.1 removed the wholesale `AuthStorage.list()` enumeration, so
5
+ * discovery probes the fixed managed-account provider-id convention
6
+ * (`anthropic`, `openai-codex`, and `google-antigravity`, plus each family's
7
+ * `-account-N` aliases) one id at a time. Only when the public surface is
8
+ * unavailable does discovery
9
+ * use the narrow read-only auth.json fallback (REQ-AUTH-1, REQ-AUTH-2).
10
+ *
11
+ * Credential values are never extracted, retained, hashed, fingerprinted, or
12
+ * logged by this module. auth.json is never mutated.
13
+ */
14
+
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import type { MultiAccountConfig } from "./config.js";
17
+ import {
18
+ MANAGED_FAMILIES,
19
+ accountSlotIndexes,
20
+ canonicalProviderIdForAccountSlot,
21
+ isAccountLimit,
22
+ isAccountSlotIndex,
23
+ isManagedFamily,
24
+ type ManagedFamily,
25
+ } from "./config.js";
26
+
27
+ export type CredentialType = "oauth" | "api_key" | "unknown";
28
+
29
+ export interface CredentialPresenceRecord {
30
+ readonly providerId: string;
31
+ /** Bounded presence category only; never an arbitrary source string. */
32
+ readonly credentialType: CredentialType;
33
+ /**
34
+ * Epoch milliseconds at which the stored credential expires, when the store
35
+ * reports one. This is a bounded timestamp, never credential material. It
36
+ * exists because Pi's OAuth refresh is lazy: an account that serves no
37
+ * request is never refreshed and silently ages out, so freshness has to be
38
+ * observable before a request is attempted.
39
+ */
40
+ readonly expiresAtMs?: number;
41
+ }
42
+
43
+ export type PublicCredentialMetadataResult =
44
+ | {
45
+ readonly status: "available";
46
+ readonly records: readonly CredentialPresenceRecord[];
47
+ }
48
+ | {
49
+ readonly status: "unavailable";
50
+ };
51
+
52
+ export interface PublicAuthStorageAdapter {
53
+ /**
54
+ * Distinguishes an authoritative public result (including an empty list)
55
+ * from an unavailable, failed, or unsupported public surface.
56
+ */
57
+ listProviders(): Promise<PublicCredentialMetadataResult>;
58
+ }
59
+
60
+ function normalizeCredentialType(value: unknown): CredentialType {
61
+ return value === "oauth" || value === "api_key" ? value : "unknown";
62
+ }
63
+
64
+ /**
65
+ * Reads a single stored credential's bounded presence metadata for one
66
+ * provider id. Returns the normalized credential TYPE only (or `null` when no
67
+ * credential is stored). Credential secret values never cross this boundary.
68
+ */
69
+ /** Bounded credential facts a probe may surface. Never a secret value. */
70
+ export interface CredentialFacts {
71
+ readonly credentialType: CredentialType;
72
+ readonly expiresAtMs?: number;
73
+ }
74
+
75
+ export type StoredCredentialProbe = (
76
+ providerId: string,
77
+ ) => CredentialType | CredentialFacts | null;
78
+
79
+ /** Accepts either probe return shape so existing probes keep working. */
80
+ function toCredentialFacts(
81
+ value: CredentialType | CredentialFacts | null,
82
+ ): CredentialFacts | null {
83
+ if (value === null) return null;
84
+ return typeof value === "string" ? { credentialType: value } : value;
85
+ }
86
+
87
+ /** Accepts only a finite, non-negative epoch timestamp. */
88
+ function boundedExpiry(value: unknown): number | undefined {
89
+ if (typeof value !== "number") return undefined;
90
+ if (!Number.isFinite(value) || value < 0) return undefined;
91
+ return value;
92
+ }
93
+
94
+ /** The fixed set of managed-account provider ids probed by convention. */
95
+ export function managedProviderIdCandidates(
96
+ accountLimit: number,
97
+ ): readonly string[] {
98
+ const candidates: string[] = [];
99
+ for (const family of MANAGED_FAMILIES) {
100
+ for (const index of accountSlotIndexes(accountLimit, 1)) {
101
+ if (!isAccountSlotIndex(index, accountLimit)) continue;
102
+ const providerId = canonicalProviderIdForAccountSlot(
103
+ family,
104
+ index,
105
+ accountLimit,
106
+ );
107
+ if (providerId !== null) candidates.push(providerId);
108
+ }
109
+ }
110
+ return candidates;
111
+ }
112
+
113
+ /**
114
+ * Probes a fixed set of candidate managed provider ids for credential presence,
115
+ * classifying and revalidating each supplied id against the current limit
116
+ * immediately before invoking `probe(providerId)`. This narrow seam lets the
117
+ * pre-probe guard be exercised with hostile canonical above-limit candidate ids
118
+ * without mutating or mocking the sole iterator. A supplied id that does not
119
+ * classify or falls outside the current limit is never probed.
120
+ */
121
+ export function probeManagedProviderIdCandidates(
122
+ candidateProviderIds: readonly string[],
123
+ probe: StoredCredentialProbe,
124
+ accountLimit: number,
125
+ ): PublicCredentialMetadataResult {
126
+ const records: CredentialPresenceRecord[] = [];
127
+ for (const providerId of candidateProviderIds) {
128
+ const slot = classifyProviderId({ providerId, credentialType: "unknown" });
129
+ if (slot === null || !isProviderSlotWithinAccountLimit(slot, accountLimit)) {
130
+ continue;
131
+ }
132
+ let facts: CredentialFacts | null;
133
+ try {
134
+ facts = toCredentialFacts(probe(providerId));
135
+ } catch {
136
+ return { status: "unavailable" };
137
+ }
138
+ if (facts !== null) {
139
+ records.push({
140
+ providerId,
141
+ credentialType: facts.credentialType,
142
+ ...(facts.expiresAtMs === undefined
143
+ ? {}
144
+ : { expiresAtMs: facts.expiresAtMs }),
145
+ });
146
+ }
147
+ }
148
+ return { status: "available", records };
149
+ }
150
+
151
+ /**
152
+ * Wraps Pi's public `readStoredCredential` surface in the adapter expected by
153
+ * discovery. Probes the managed-account provider-id convention one id at a
154
+ * time; a provider is "present" when the probe returns a credential type.
155
+ * Credential values and arbitrary credential-type strings never cross the
156
+ * adapter boundary — only the provider id and a bounded presence category do.
157
+ */
158
+ export function createPublicAuthStorageAdapter(
159
+ probe: StoredCredentialProbe | null | undefined,
160
+ accountLimit: number,
161
+ ): PublicAuthStorageAdapter {
162
+ return {
163
+ async listProviders(): Promise<PublicCredentialMetadataResult> {
164
+ if (typeof probe !== "function") {
165
+ return { status: "unavailable" };
166
+ }
167
+
168
+ return probeManagedProviderIdCandidates(
169
+ managedProviderIdCandidates(accountLimit),
170
+ probe,
171
+ accountLimit,
172
+ );
173
+ },
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Builds a {@link StoredCredentialProbe} over Pi's public
179
+ * `readStoredCredential(providerId, authPath?)`. The reader returns a
180
+ * `Credential | undefined`; this probe surfaces only the normalized `.type`
181
+ * (never the secret value) and maps a missing credential to `null`.
182
+ */
183
+ export function createReadStoredCredentialProbe(
184
+ readStoredCredential: (providerId: string, authPath?: string) => unknown,
185
+ authPath?: string,
186
+ ): StoredCredentialProbe {
187
+ return (providerId: string): CredentialFacts | null => {
188
+ const credential = readStoredCredential(providerId, authPath);
189
+ if (credential === null || typeof credential !== "object") {
190
+ return null;
191
+ }
192
+ const record = credential as Record<string, unknown>;
193
+ const expiresAtMs = boundedExpiry(record["expires"]);
194
+ return {
195
+ credentialType: normalizeCredentialType(record["type"]),
196
+ ...(expiresAtMs === undefined ? {} : { expiresAtMs }),
197
+ };
198
+ };
199
+ }
200
+
201
+ export interface AuthJsonFallbackAdapter {
202
+ /** Returns only provider ID and bounded presence metadata. Never writes. */
203
+ listProviders(authPath: string): readonly CredentialPresenceRecord[];
204
+ }
205
+
206
+ export const DEFAULT_AUTH_JSON_FALLBACK: AuthJsonFallbackAdapter = {
207
+ listProviders(authPath: string): readonly CredentialPresenceRecord[] {
208
+ if (!existsSync(authPath)) return [];
209
+
210
+ let parsed: unknown;
211
+ try {
212
+ parsed = JSON.parse(readFileSync(authPath, "utf-8"));
213
+ } catch {
214
+ return [];
215
+ }
216
+
217
+ if (
218
+ typeof parsed !== "object" ||
219
+ parsed === null ||
220
+ Array.isArray(parsed)
221
+ ) {
222
+ return [];
223
+ }
224
+
225
+ const records: CredentialPresenceRecord[] = [];
226
+ for (const [providerId, value] of Object.entries(parsed)) {
227
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
228
+ continue;
229
+ }
230
+ const record = value as Record<string, unknown>;
231
+ const credentialType = normalizeCredentialType(record["type"]);
232
+ const expiresAtMs = boundedExpiry(record["expires"]);
233
+ records.push({
234
+ providerId,
235
+ credentialType,
236
+ ...(expiresAtMs === undefined ? {} : { expiresAtMs }),
237
+ });
238
+ }
239
+ return records;
240
+ },
241
+ };
242
+
243
+ const ANTHROPIC_BASE_PROVIDER = "anthropic";
244
+ const CODEX_BASE_PROVIDER = "openai-codex";
245
+ const ANTIGRAVITY_BASE_PROVIDER = "google-antigravity";
246
+ const OPENAI_BASE_PROVIDER = "openai";
247
+ const NUMBERED_SLOT_PATTERN =
248
+ /^(anthropic|openai-codex|google-antigravity|openai)-account-([2-9]|[1-9]\d+)$/;
249
+
250
+ export interface ProviderSlot {
251
+ readonly family: ManagedFamily;
252
+ readonly providerId: string;
253
+ /** Numeric slot index: 1 for the base provider, 2+ for aliases. */
254
+ readonly slotIndex: number;
255
+ readonly credentialType: CredentialType;
256
+ /** Bounded credential expiry, when the credential store reports one. */
257
+ readonly expiresAtMs?: number;
258
+ }
259
+
260
+ /** Maps only canonical base and `*-account-N` provider IDs to slots. */
261
+ export function classifyProviderId(
262
+ record: CredentialPresenceRecord,
263
+ ): ProviderSlot | null {
264
+ const { providerId, credentialType } = record;
265
+ const expiry =
266
+ record.expiresAtMs === undefined ? {} : { expiresAtMs: record.expiresAtMs };
267
+
268
+ if (providerId === ANTHROPIC_BASE_PROVIDER) {
269
+ return {
270
+ family: "anthropic",
271
+ providerId,
272
+ slotIndex: 1,
273
+ credentialType,
274
+ ...expiry,
275
+ };
276
+ }
277
+ if (providerId === CODEX_BASE_PROVIDER) {
278
+ return {
279
+ family: "openai-codex",
280
+ providerId,
281
+ slotIndex: 1,
282
+ credentialType,
283
+ ...expiry,
284
+ };
285
+ }
286
+ if (providerId === ANTIGRAVITY_BASE_PROVIDER) {
287
+ return {
288
+ family: "google-antigravity",
289
+ providerId,
290
+ slotIndex: 1,
291
+ credentialType,
292
+ ...expiry,
293
+ };
294
+ }
295
+ if (providerId === OPENAI_BASE_PROVIDER) {
296
+ return {
297
+ family: "openai",
298
+ providerId,
299
+ slotIndex: 1,
300
+ credentialType,
301
+ ...expiry,
302
+ };
303
+ }
304
+
305
+ const match = NUMBERED_SLOT_PATTERN.exec(providerId);
306
+ if (!match) return null;
307
+ const family = match[1];
308
+ const rawSlotIndex = match[2];
309
+ if (!family || !rawSlotIndex) return null;
310
+ const slotIndex = Number(rawSlotIndex);
311
+ if (!Number.isSafeInteger(slotIndex) || slotIndex < 2) return null;
312
+ if (!isManagedFamily(family)) return null;
313
+
314
+ return {
315
+ family,
316
+ providerId,
317
+ slotIndex,
318
+ credentialType,
319
+ ...expiry,
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Whether a classified slot is within the current validated account limit. A
325
+ * non-iterating structural predicate over named `{ providerId, family,
326
+ * slotIndex }` fields: it requires the slot to round-trip through
327
+ * `canonicalProviderIdForAccountSlot(family, slotIndex, accountLimit)`, so a
328
+ * hostile record whose `providerId`, `family`, and `slotIndex` disagree, or
329
+ * whose index exceeds the limit, is rejected. `classifyProviderId()` stays
330
+ * deliberately limit-agnostic so fallback can parse a legacy high-slot id long
331
+ * enough to reject it here at a caller-owned boundary.
332
+ */
333
+ export function isProviderSlotWithinAccountLimit(
334
+ slot: Pick<ProviderSlot, "providerId" | "family" | "slotIndex">,
335
+ accountLimit: number,
336
+ ): boolean {
337
+ return (
338
+ canonicalProviderIdForAccountSlot(
339
+ slot.family,
340
+ slot.slotIndex,
341
+ accountLimit,
342
+ ) === slot.providerId
343
+ );
344
+ }
345
+
346
+ export interface DiscoveryResult {
347
+ readonly slots: readonly ProviderSlot[];
348
+ readonly usedPublicMetadata: boolean;
349
+ readonly spareSlots: ReadonlyMap<ManagedFamily, string>;
350
+ }
351
+
352
+ function nextFreeSlotId(
353
+ family: ManagedFamily,
354
+ existing: readonly ProviderSlot[],
355
+ limit: number,
356
+ ): string | null {
357
+ const occupied = new Set<number>();
358
+ for (const slot of existing) {
359
+ if (slot.family === family) occupied.add(slot.slotIndex);
360
+ }
361
+ for (const slotIndex of accountSlotIndexes(limit, 1)) {
362
+ if (occupied.has(slotIndex)) continue;
363
+ if (!isAccountSlotIndex(slotIndex, limit)) continue;
364
+ const providerId = canonicalProviderIdForAccountSlot(
365
+ family,
366
+ slotIndex,
367
+ limit,
368
+ );
369
+ if (providerId !== null) return providerId;
370
+ }
371
+ return null;
372
+ }
373
+
374
+ function compareSlots(left: ProviderSlot, right: ProviderSlot): number {
375
+ const familyOrder =
376
+ MANAGED_FAMILIES.indexOf(left.family) -
377
+ MANAGED_FAMILIES.indexOf(right.family);
378
+ return familyOrder !== 0
379
+ ? familyOrder
380
+ : left.slotIndex - right.slotIndex ||
381
+ left.providerId.localeCompare(right.providerId);
382
+ }
383
+
384
+ /**
385
+ * Discovers canonical slots from authoritative public metadata or, only when
386
+ * that public surface is unavailable, the read-only fallback. Exact provider
387
+ * IDs are deduplicated; no real-account identity equivalence is inferred.
388
+ */
389
+ export async function discoverAccounts(options: {
390
+ publicAdapter: PublicAuthStorageAdapter;
391
+ fallbackAdapter?: AuthJsonFallbackAdapter;
392
+ authJsonPath?: string;
393
+ config: MultiAccountConfig;
394
+ }): Promise<DiscoveryResult> {
395
+ const {
396
+ publicAdapter,
397
+ fallbackAdapter = DEFAULT_AUTH_JSON_FALLBACK,
398
+ authJsonPath,
399
+ config,
400
+ } = options;
401
+
402
+ let publicResult: PublicCredentialMetadataResult;
403
+ try {
404
+ publicResult = await publicAdapter.listProviders();
405
+ } catch {
406
+ publicResult = { status: "unavailable" };
407
+ }
408
+
409
+ let records: readonly CredentialPresenceRecord[];
410
+ let usedPublicMetadata: boolean;
411
+ if (publicResult.status === "available") {
412
+ records = publicResult.records;
413
+ usedPublicMetadata = true;
414
+ } else if (authJsonPath) {
415
+ records = fallbackAdapter.listProviders(authJsonPath);
416
+ usedPublicMetadata = false;
417
+ } else {
418
+ records = [];
419
+ usedPublicMetadata = false;
420
+ }
421
+
422
+ const seenProviderIds = new Set<string>();
423
+ const slots: ProviderSlot[] = [];
424
+ if (usedPublicMetadata) {
425
+ // The public probe already visits only in-range candidates, but a hostile
426
+ // or unavailable-then-fabricated public result is defended in depth: each
427
+ // classified slot must pass the current-limit predicate before projection.
428
+ for (const record of records) {
429
+ if (seenProviderIds.has(record.providerId)) continue;
430
+ seenProviderIds.add(record.providerId);
431
+ const slot = classifyProviderId(record);
432
+ if (slot && isProviderSlotWithinAccountLimit(slot, config.accountLimit)) {
433
+ slots.push(slot);
434
+ }
435
+ }
436
+ } else {
437
+ // The read-only fallback can name a legacy above-limit id. Build the set of
438
+ // canonical provider ids allowed for the current limit from the sole
439
+ // iterator, revalidating each yield before the canonical formatter, then
440
+ // admit only a classified fallback record whose id is a member.
441
+ const allowedProviderIds = new Set<string>();
442
+ for (const family of MANAGED_FAMILIES) {
443
+ for (const index of accountSlotIndexes(config.accountLimit, 1)) {
444
+ if (!isAccountSlotIndex(index, config.accountLimit)) continue;
445
+ const providerId = canonicalProviderIdForAccountSlot(
446
+ family,
447
+ index,
448
+ config.accountLimit,
449
+ );
450
+ if (providerId !== null) allowedProviderIds.add(providerId);
451
+ }
452
+ }
453
+ for (const record of records) {
454
+ if (seenProviderIds.has(record.providerId)) continue;
455
+ seenProviderIds.add(record.providerId);
456
+ const slot = classifyProviderId(record);
457
+ if (slot && allowedProviderIds.has(slot.providerId)) {
458
+ slots.push(slot);
459
+ }
460
+ }
461
+ }
462
+ slots.sort(compareSlots);
463
+
464
+ const spareSlots = new Map<ManagedFamily, string>();
465
+ for (const family of MANAGED_FAMILIES) {
466
+ const spare = nextFreeSlotId(family, slots, config.accountLimit);
467
+ if (spare !== null) spareSlots.set(family, spare);
468
+ }
469
+
470
+ return { slots, usedPublicMetadata, spareSlots };
471
+ }
@@ -0,0 +1,13 @@
1
+ /** Formats a non-negative millisecond interval for bounded operator display. */
2
+ export function formatDuration(ms: number): string {
3
+ const seconds = Math.max(0, Math.round(ms / 1_000));
4
+ if (seconds < 60) return `${seconds}s`;
5
+ const minutes = Math.floor(seconds / 60);
6
+ if (minutes < 60) {
7
+ const rest = seconds % 60;
8
+ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
9
+ }
10
+ const hours = Math.floor(minutes / 60);
11
+ const restMinutes = minutes % 60;
12
+ return restMinutes === 0 ? `${hours}h` : `${hours}h ${restMinutes}m`;
13
+ }