@askalf/dario 6.12.7 → 6.12.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -536,8 +536,9 @@ async function proxy() {
536
536
  ?? fileCfg.pool?.strategy;
537
537
  // --pool-headroom-floor=<ratio|percent> — the headroom at/below which a seat
538
538
  // counts as drained: sticky sessions rebind off it and new conversations
539
- // skip it. Default 2%; dario#1333 asked to leave a seat alone at 95% used
540
- // rather than ride it into the 429.
539
+ // skip it. Setting the floor makes it a hard stop; the implicit 2% default
540
+ // stays a routing preference. dario#1333 asked to leave a seat alone at 95%
541
+ // used rather than ride it into the 429.
541
542
  const poolHeadroomFloorFromFlag = args.find((a) => a.startsWith('--pool-headroom-floor='))?.split('=')[1];
542
543
  if (poolHeadroomFloorFromFlag !== undefined && parsePoolHeadroomFloor(poolHeadroomFloorFromFlag) === null) {
543
544
  console.error(`[dario] Invalid --pool-headroom-floor "${poolHeadroomFloorFromFlag}". Use a ratio or percent between 2% and 50% (e.g. 0.05 or 5%).`);
@@ -2203,8 +2204,9 @@ async function help() {
2203
2204
  --pool-headroom-floor=<ratio|percent>
2204
2205
  Headroom at or below which a seat counts as
2205
2206
  drained: a sticky session rebinds off it and
2206
- new conversations skip it. 0.05 or 5%;
2207
- default 2%, max 50%. Use it when your seats
2207
+ new conversations skip it. Setting this makes
2208
+ the floor a hard stop. 0.05 or 5%; implicit
2209
+ default 2% is a preference, max 50%. Use it when your seats
2208
2210
  answer with API errors in the last percent
2209
2211
  of a window (dario#1333).
2210
2212
  Env: DARIO_POOL_HEADROOM_FLOOR;
@@ -1139,7 +1139,7 @@ export async function runChecks(opts = {}) {
1139
1139
  // default: `new AccountPool()` here always predicted a max-headroom
1140
1140
  // pick, which is the wrong seat on a fill-first pool.
1141
1141
  const routing = configuredPoolRouting(loadConfig().config.pool);
1142
- const pool = new AccountPool(routing.strategy, routing.headroomFloor);
1142
+ const pool = new AccountPool(routing.strategy, routing.headroomFloorExplicit ? routing.headroomFloor : undefined);
1143
1143
  for (const acc of loaded) {
1144
1144
  pool.add(acc.alias, {
1145
1145
  accessToken: acc.accessToken,
package/dist/pool.d.ts CHANGED
@@ -437,6 +437,7 @@ export declare function configuredPoolRouting(file: {
437
437
  } | undefined, env?: NodeJS.ProcessEnv): {
438
438
  strategy: PoolStrategy;
439
439
  headroomFloor: number;
440
+ headroomFloorExplicit: boolean;
440
441
  };
441
442
  /**
442
443
  * Which wire buckets bind which model family, when the wire does not say it
@@ -602,8 +603,6 @@ export declare function resolvePoolHeadroomFloor(explicit?: string | number | nu
602
603
  export declare function seatExpiry(account: PoolAccount, now?: number): number;
603
604
  export declare class AccountPool {
604
605
  readonly strategy: PoolStrategy;
605
- /** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
606
- readonly headroomFloor: number;
607
606
  private accounts;
608
607
  private queue;
609
608
  private queueMaxSize;
@@ -611,9 +610,11 @@ export declare class AccountPool {
611
610
  private drainTimer;
612
611
  private sticky;
613
612
  private lastStickyCleanup;
614
- constructor(strategy?: PoolStrategy,
615
613
  /** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
616
- headroomFloor?: number);
614
+ readonly headroomFloor: number;
615
+ /** Only an explicitly configured floor is a hard stop; the implicit 2% remains a preference. */
616
+ readonly hardHeadroomFloor: boolean;
617
+ constructor(strategy?: PoolStrategy, headroomFloor?: number);
617
618
  add(alias: string, opts: {
618
619
  accessToken: string;
619
620
  refreshToken: string;
@@ -729,6 +730,8 @@ export declare class AccountPool {
729
730
  adoptSnapshot(alias: string, snapshot: RateLimitSnapshot, rejected: boolean, from: string): boolean;
730
731
  updateTokens(alias: string, accessToken: string, refreshToken: string, expiresAt: number): void;
731
732
  get(alias: string): PoolAccount | undefined;
733
+ /** Whether a preferred (but not pinned) seat may bypass normal pool ordering. */
734
+ canPrefer(account: PoolAccount, now?: number, family?: string | null): boolean;
732
735
  all(): PoolAccount[];
733
736
  status(): PoolStatus;
734
737
  /**
package/dist/pool.js CHANGED
@@ -453,9 +453,11 @@ export function describePoolStrategy(strategy, floor = DEFAULT_POOL_HEADROOM_FLO
453
453
  * CLI flag, which only the running process knows (GET /status reports it).
454
454
  */
455
455
  export function configuredPoolRouting(file, env = process.env) {
456
+ const floorCandidate = env.DARIO_POOL_HEADROOM_FLOOR ?? file?.headroomFloor;
456
457
  return {
457
458
  strategy: resolvePoolStrategy(env.DARIO_POOL_STRATEGY ?? file?.strategy, env),
458
- headroomFloor: resolvePoolHeadroomFloor(env.DARIO_POOL_HEADROOM_FLOOR ?? file?.headroomFloor, env),
459
+ headroomFloor: resolvePoolHeadroomFloor(floorCandidate, env),
460
+ headroomFloorExplicit: parsePoolHeadroomFloor(floorCandidate) !== null,
459
461
  };
460
462
  }
461
463
  /**
@@ -834,11 +836,56 @@ function pickMaxHeadroom(accounts, family) {
834
836
  }
835
837
  return best;
836
838
  }
839
+ function isAboveHeadroomFloor(headroom, floor) {
840
+ return headroom - floor > Number.EPSILON;
841
+ }
842
+ /**
843
+ * When a below-floor reading can retire without an upstream probe, return the
844
+ * first relevant reset in epoch ms. Null means at least one bucket keeping the
845
+ * seat below the floor has no future reset, so one request must refresh it.
846
+ */
847
+ function floorParkedUntil(snapshot, family, floor, now) {
848
+ const rl = expireElapsedWindow(snapshot, now);
849
+ const limiting = (util) => !isAboveHeadroomFloor(1 - util, floor);
850
+ const resets = [];
851
+ if (limiting(rl.util5h)) {
852
+ const reset = rl.claim === 'five_hour' ? rl.reset * 1000 : 0;
853
+ if (reset <= now)
854
+ return null;
855
+ resets.push(reset);
856
+ }
857
+ if (limiting(rl.util7d)) {
858
+ const reset = (rl.reset7d ?? 0) * 1000 > now
859
+ ? (rl.reset7d ?? 0) * 1000
860
+ : rl.claim.startsWith('seven_day') ? rl.reset * 1000 : 0;
861
+ if (reset <= now)
862
+ return null;
863
+ resets.push(reset);
864
+ }
865
+ if (family) {
866
+ for (const bucket of bucketsBindingFamily(rl, family)) {
867
+ const util = rl.perModel7d[bucket];
868
+ if (util === undefined || !limiting(util))
869
+ continue;
870
+ const reset = (rl.reset7d ?? 0) * 1000 > now
871
+ ? (rl.reset7d ?? 0) * 1000
872
+ : rl.claim.startsWith('seven_day') ? rl.reset * 1000 : 0;
873
+ if (reset <= now)
874
+ return null;
875
+ resets.push(reset);
876
+ }
877
+ }
878
+ return resets.length > 0 ? Math.min(...resets) : null;
879
+ }
880
+ function pickFloorProbe(accounts, family, floor, now) {
881
+ const stale = accounts.filter(a => floorParkedUntil(a.rateLimit, family, floor, now) === null);
882
+ return stale.length > 0 ? pickMaxHeadroom(stale, family) : null;
883
+ }
837
884
  // Fill-first pick: lexicographically-first eligible account still above the
838
885
  // headroom floor. Alias order (not insertion order) — accounts load from a
839
886
  // readdir whose order the OS doesn't guarantee, and the operator can control
840
887
  // alias names but not readdir. Returns null when every candidate is at/below
841
- // the floor so the caller can fall back to max-headroom.
888
+ // the floor.
842
889
  /**
843
890
  * The 7-day reset `expiring-first` orders by, in epoch ms: the seat's own
844
891
  * reading while it is still ahead of `now`, else +Infinity (never read, or the
@@ -851,7 +898,7 @@ export function seatExpiry(account, now = Date.now()) {
851
898
  function pickExpiringFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR, now = Date.now()) {
852
899
  const order = [...accounts].sort((a, b) => (seatExpiry(a, now) - seatExpiry(b, now)) || (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
853
900
  for (const a of order) {
854
- if (computeHeadroom(a.rateLimit, family) > floor)
901
+ if (isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family), floor))
855
902
  return a;
856
903
  }
857
904
  return null;
@@ -861,14 +908,13 @@ function pickFillFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR) {
861
908
  for (const a of accounts) {
862
909
  if (best !== null && a.alias >= best.alias)
863
910
  continue;
864
- if (computeHeadroom(a.rateLimit, family) > floor)
911
+ if (isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family), floor))
865
912
  best = a;
866
913
  }
867
914
  return best;
868
915
  }
869
916
  export class AccountPool {
870
917
  strategy;
871
- headroomFloor;
872
918
  accounts = new Map();
873
919
  queue = [];
874
920
  queueMaxSize = 50;
@@ -877,11 +923,14 @@ export class AccountPool {
877
923
  sticky = new Map();
878
924
  // Amortize the O(n) sticky TTL/orphan sweep — timestamp of the last run.
879
925
  lastStickyCleanup = 0;
880
- constructor(strategy = 'headroom',
881
926
  /** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
882
- headroomFloor = DEFAULT_POOL_HEADROOM_FLOOR) {
927
+ headroomFloor;
928
+ /** Only an explicitly configured floor is a hard stop; the implicit 2% remains a preference. */
929
+ hardHeadroomFloor;
930
+ constructor(strategy = 'headroom', headroomFloor) {
883
931
  this.strategy = strategy;
884
- this.headroomFloor = headroomFloor;
932
+ this.headroomFloor = headroomFloor ?? DEFAULT_POOL_HEADROOM_FLOOR;
933
+ this.hardHeadroomFloor = headroomFloor !== undefined;
885
934
  }
886
935
  add(alias, opts) {
887
936
  const existing = this.accounts.get(alias);
@@ -997,11 +1046,13 @@ export class AccountPool {
997
1046
  : pickExpiringFirst(eligible, family, this.headroomFloor, now);
998
1047
  if (first)
999
1048
  return first;
1000
- // Every eligible account is at/below the floor — the terminal state
1001
- // both strategies share. Fall through to max-headroom so the caller
1002
- // still gets the least-drained account instead of null.
1003
1049
  }
1004
- return pickMaxHeadroom(eligible, family);
1050
+ if (!this.hardHeadroomFloor)
1051
+ return pickMaxHeadroom(eligible, family);
1052
+ const aboveFloor = eligible.filter(a => isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family, now), this.headroomFloor));
1053
+ return aboveFloor.length > 0
1054
+ ? pickMaxHeadroom(aboveFloor, family)
1055
+ : pickFloorProbe(eligible, family, this.headroomFloor, now);
1005
1056
  }
1006
1057
  // No seat is eligible. A seat parked inside a live window is not
1007
1058
  // re-probed: its 429 named the reset, the clock has not reached it, and a
@@ -1042,11 +1093,20 @@ export class AccountPool {
1042
1093
  // 429 again (dario#1264 review). An auth cool-down or an expired token
1043
1094
  // still does NOT count — those are not rate limits and must not be
1044
1095
  // reported, or cooled, as if they were.
1045
- if (!all.every(a => isParkedInLiveWindow(a, now, family) || isCoolingAfterRejection(a, now)))
1096
+ const until = all.map((a) => {
1097
+ if (isParkedInLiveWindow(a, now, family)) {
1098
+ return isBucketScopedRejection(a.rateLimit)
1099
+ ? (a.rateLimit.parkedBucketsUntil ?? a.rateLimit.reset * 1000)
1100
+ : a.rateLimit.reset * 1000;
1101
+ }
1102
+ if (isCoolingAfterRejection(a, now))
1103
+ return a.rateLimit.cooldownUntil ?? now;
1104
+ if (this.hardHeadroomFloor && isAccountEligible(a, now, family)) {
1105
+ return floorParkedUntil(a.rateLimit, family, this.headroomFloor, now);
1106
+ }
1046
1107
  return null;
1047
- return Math.min(...all.map(a => isParkedInLiveWindow(a, now, family)
1048
- ? (isBucketScopedRejection(a.rateLimit) ? (a.rateLimit.parkedBucketsUntil ?? a.rateLimit.reset * 1000) : a.rateLimit.reset * 1000)
1049
- : a.rateLimit.cooldownUntil ?? now));
1108
+ });
1109
+ return until.every((reset) => reset !== null) ? Math.min(...until) : null;
1050
1110
  }
1051
1111
  /** Seats a rate limit is currently keeping out of rotation (dario#1244, #1264). */
1052
1112
  parkedCount(now = Date.now()) {
@@ -1077,7 +1137,7 @@ export class AccountPool {
1077
1137
  const bound = this.accounts.get(binding.alias);
1078
1138
  if (bound
1079
1139
  && isAccountEligible(bound, now, family)
1080
- && computeHeadroom(bound.rateLimit, family) > this.headroomFloor) {
1140
+ && isAboveHeadroomFloor(computeHeadroom(bound.rateLimit, family), this.headroomFloor)) {
1081
1141
  // Refresh the idle timer. A session that keeps taking turns must never
1082
1142
  // be reaped or rebound while active — that would strand its warm prompt
1083
1143
  // cache — so the TTL is re-based to now on every hit.
@@ -1167,7 +1227,12 @@ export class AccountPool {
1167
1227
  if (first)
1168
1228
  return first;
1169
1229
  }
1170
- return pickMaxHeadroom(eligible, family);
1230
+ if (!this.hardHeadroomFloor)
1231
+ return pickMaxHeadroom(eligible, family);
1232
+ const aboveFloor = eligible.filter(a => isAboveHeadroomFloor(computeHeadroom(a.rateLimit, family, now), this.headroomFloor));
1233
+ return aboveFloor.length > 0
1234
+ ? pickMaxHeadroom(aboveFloor, family)
1235
+ : pickFloorProbe(eligible, family, this.headroomFloor, now);
1171
1236
  }
1172
1237
  // Mid-flight: the seats a 429 could still hand this request to. A seat
1173
1238
  // parked inside a live window is not one of them — on the dario#1244
@@ -1280,6 +1345,18 @@ export class AccountPool {
1280
1345
  get(alias) {
1281
1346
  return this.accounts.get(alias);
1282
1347
  }
1348
+ /** Whether a preferred (but not pinned) seat may bypass normal pool ordering. */
1349
+ canPrefer(account, now = Date.now(), family) {
1350
+ if (!isAccountEligible(account, now, family))
1351
+ return false;
1352
+ if (!this.hardHeadroomFloor)
1353
+ return true;
1354
+ if (isAboveHeadroomFloor(computeHeadroom(account.rateLimit, family, now), this.headroomFloor))
1355
+ return true;
1356
+ // A stale below-floor reading needs one probe to refresh it; a reading
1357
+ // with a known future reset remains hard-parked until that reset.
1358
+ return floorParkedUntil(account.rateLimit, family, this.headroomFloor, now) === null;
1359
+ }
1283
1360
  all() {
1284
1361
  return [...this.accounts.values()];
1285
1362
  }
@@ -1311,11 +1388,9 @@ export class AccountPool {
1311
1388
  */
1312
1389
  async waitForAccount() {
1313
1390
  const immediate = this.select();
1314
- if (immediate) {
1315
- const headroom = computeHeadroom(immediate.rateLimit);
1316
- if (headroom > this.headroomFloor)
1317
- return immediate;
1318
- }
1391
+ if (immediate && (this.hardHeadroomFloor
1392
+ || isAboveHeadroomFloor(computeHeadroom(immediate.rateLimit), this.headroomFloor)))
1393
+ return immediate;
1319
1394
  if (this.queue.length >= this.queueMaxSize) {
1320
1395
  throw new Error('Queue full — all accounts exhausted');
1321
1396
  }
@@ -1355,8 +1430,10 @@ export class AccountPool {
1355
1430
  const account = this.select();
1356
1431
  if (!account)
1357
1432
  break;
1358
- const headroom = computeHeadroom(account.rateLimit);
1359
- if (headroom <= this.headroomFloor)
1433
+ // The implicit floor keeps its historical queueing behavior. In hard
1434
+ // mode select() has already applied the floor, including stale probes.
1435
+ if (!this.hardHeadroomFloor
1436
+ && !isAboveHeadroomFloor(computeHeadroom(account.rateLimit), this.headroomFloor))
1360
1437
  break;
1361
1438
  const entry = this.queue.shift();
1362
1439
  if (entry)
package/dist/proxy.d.ts CHANGED
@@ -436,7 +436,8 @@ interface ProxyOptions {
436
436
  /**
437
437
  * Headroom at/below which a seat counts as drained (dario#1333): a sticky
438
438
  * session rebinds off it and new conversations skip it. Ratio (`0.05`) or
439
- * percent (`5%`); default 2%. Sourced from `--pool-headroom-floor` /
439
+ * percent (`5%`). The implicit 2% default is a routing preference; setting
440
+ * this option explicitly makes the floor a hard stop. Sourced from `--pool-headroom-floor` /
440
441
  * `DARIO_POOL_HEADROOM_FLOOR` / config `pool.headroomFloor`.
441
442
  */
442
443
  poolHeadroomFloor?: string | number;
package/dist/proxy.js CHANGED
@@ -13,7 +13,7 @@ import { CC_TOOL_DEFINITIONS_UNADVERTISABLE, CC_TEMPLATE_PROMPT_BYTES, resolveMa
13
13
  import { stampCch, hasCchSeed } from './cch.js';
14
14
  import { foldTiming, timingHeaders, timingLogFields } from './timing.js';
15
15
  import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion } from './live-fingerprint.js';
16
- import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, activeParkedBuckets, reconcilePoolAccounts, resolvePoolStrategy, resolvePoolHeadroomFloor, describePoolStrategy, DEFAULT_POOL_HEADROOM_FLOOR, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail, isAccountEligible } from './pool.js';
16
+ import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, activeParkedBuckets, reconcilePoolAccounts, resolvePoolStrategy, resolvePoolHeadroomFloor, parsePoolHeadroomFloor, describePoolStrategy, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail } from './pool.js';
17
17
  import { backfillIdentity } from './accounts.js';
18
18
  import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
19
19
  import { Analytics, billingBucketFromClaim, costOfTokensFailClosed, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
@@ -1514,8 +1514,9 @@ export async function startProxy(opts = {}) {
1514
1514
  const accountsList = await loadAllAccounts();
1515
1515
  const poolStrategy = resolvePoolStrategy(opts.poolStrategy);
1516
1516
  const poolHeadroomFloor = resolvePoolHeadroomFloor(opts.poolHeadroomFloor);
1517
- const pool = new AccountPool(poolStrategy, poolHeadroomFloor);
1518
- if (poolHeadroomFloor !== DEFAULT_POOL_HEADROOM_FLOOR) {
1517
+ const poolHeadroomFloorExplicit = parsePoolHeadroomFloor(opts.poolHeadroomFloor) !== null;
1518
+ const pool = new AccountPool(poolStrategy, poolHeadroomFloorExplicit ? poolHeadroomFloor : undefined);
1519
+ if (poolHeadroomFloorExplicit) {
1519
1520
  console.log(`[dario] Pool headroom floor: ${Math.round(poolHeadroomFloor * 100)}% — a seat at or below it is left alone: sticky sessions rebind off it, new conversations skip it`);
1520
1521
  }
1521
1522
  // Two aliases that are one account (same OAuth account uuid) are one
@@ -3391,7 +3392,7 @@ export async function startProxy(opts = {}) {
3391
3392
  // eligible right now, else the pool picks as usual. Failover
3392
3393
  // mid-request is unchanged either way — a preference, not a pin.
3393
3394
  const preferredSeat = requestAuth.key?.seat ? (pool.get(requestAuth.key.seat) ?? null) : null;
3394
- keySeatTaken = preferredSeat !== null && isAccountEligible(preferredSeat, Date.now(), requestFamily);
3395
+ keySeatTaken = preferredSeat !== null && pool.canPrefer(preferredSeat, Date.now(), requestFamily);
3395
3396
  poolAccount = keySeatTaken ? preferredSeat : pool.select(requestFamily);
3396
3397
  if (poolAccount)
3397
3398
  poolParkedAnnounced = false;
@@ -35,7 +35,7 @@ ChatGPT plans added with `dario codex add <alias>` pool the same way. The Codex
35
35
 
36
36
  Earlier releases placed ChatGPT conversations fill-first in alias order with cool-down eviction only, on the belief that the backend reported nothing before a 429, so a second seat served nothing until the first one declined.
37
37
 
38
- `--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups; `--pool-strategy=expiring-first` fills in order of each seat's 7-day reset, soonest first, so capacity that expires soonest is spent first. `--pool-headroom-floor=5%` (env `DARIO_POOL_HEADROOM_FLOOR`, config `pool.headroomFloor`; default 2%) moves the line at which a seat counts as drained: a sticky session rebinds off it and new conversations skip it once its headroom is at or below the floor, so a seat that answers with API errors in its last percent is left alone before the 429, not at it. Refresh tokens expire about 28 days after the original grant regardless of rotation, so every seat's grant age is tracked and surfaced in `dario accounts list`, `dario doctor` and `GET /accounts` before it becomes a silent outage. Provision over HTTP with the headless [admin API](admin-api.md); pin one request to one seat with `dario accounts check <alias>` (admin API required: `DARIO_ADMIN=1` and a `DARIO_ADMIN_TOKEN`). Internals and the live `/accounts` + `/analytics` endpoints: [multi-account-pool.md](multi-account-pool.md); covered end-to-end by [`test/pool-e2e.mjs`](../test/pool-e2e.mjs).
38
+ `--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups; `--pool-strategy=expiring-first` fills in order of each seat's 7-day reset, soonest first, so capacity that expires soonest is spent first. `--pool-headroom-floor=5%` (env `DARIO_POOL_HEADROOM_FLOOR`, config `pool.headroomFloor`) moves the line at which a seat counts as drained: a sticky session rebinds off it and new conversations skip it once its headroom is at or below the floor, so a seat that answers with API errors in its last percent is left alone before the 429, not at it. Setting the floor explicitly makes it a hard stop; the implicit 2% default remains a routing preference and still serves the least-drained seat when it is the only choice. A hard-parked pool answers 429 locally with `retry-after` at the earliest known reset; a reading with no reset stays probeable so stale utilization cannot park a seat forever. Refresh tokens expire about 28 days after the original grant regardless of rotation, so every seat's grant age is tracked and surfaced in `dario accounts list`, `dario doctor` and `GET /accounts` before it becomes a silent outage. Provision over HTTP with the headless [admin API](admin-api.md); pin one request to one seat with `dario accounts check <alias>` (admin API required: `DARIO_ADMIN=1` and a `DARIO_ADMIN_TOKEN`). Internals and the live `/accounts` + `/analytics` endpoints: [multi-account-pool.md](multi-account-pool.md); covered end-to-end by [`test/pool-e2e.mjs`](../test/pool-e2e.mjs).
39
39
 
40
40
  ## One key per developer
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.12.7",
3
+ "version": "6.12.8",
4
4
  "description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
5
5
  "type": "module",
6
6
  "bin": {