@askalf/dario 6.10.5 → 6.11.0

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
@@ -520,11 +520,14 @@ async function proxy() {
520
520
  // `fill-first` concentrates them on the alphabetically-first eligible seat
521
521
  // until it drains to the 2% floor, then spills — primary/backup semantics,
522
522
  // alias naming (`1-main`, `2-overflow`) is the ordering knob. Sticky
523
- // bindings behave identically under both.
523
+ // bindings behave identically under both. `expiring-first` is fill-first
524
+ // ordered by each seat's 7-day reset, soonest first: spend the capacity that
525
+ // expires soonest before touching the rest.
524
526
  const poolStrategyFromFlag = args.find((a) => a.startsWith('--pool-strategy='))?.split('=')[1];
525
527
  if (poolStrategyFromFlag !== undefined
526
- && poolStrategyFromFlag !== 'headroom' && poolStrategyFromFlag !== 'fill-first') {
527
- console.error(`[dario] Invalid --pool-strategy "${poolStrategyFromFlag}". Must be headroom or fill-first.`);
528
+ && poolStrategyFromFlag !== 'headroom' && poolStrategyFromFlag !== 'fill-first'
529
+ && poolStrategyFromFlag !== 'expiring-first') {
530
+ console.error(`[dario] Invalid --pool-strategy "${poolStrategyFromFlag}". Must be headroom, fill-first or expiring-first.`);
528
531
  process.exit(1);
529
532
  }
530
533
  const poolStrategy = poolStrategyFromFlag
@@ -2171,13 +2174,15 @@ async function help() {
2171
2174
  dario returns 504 "queue-timeout"
2172
2175
  (default: 60000).
2173
2176
  Env: DARIO_QUEUE_TIMEOUT_MS. (dario#80)
2174
- --pool-strategy=<headroom|fill-first>
2177
+ --pool-strategy=<headroom|fill-first|expiring-first>
2175
2178
  Where new conversations land in a multi-
2176
2179
  account pool. headroom (default) spreads
2177
2180
  them to the seat with the most slack;
2178
2181
  fill-first concentrates them on the
2179
2182
  alphabetically-first eligible seat until
2180
- it drains to the 2% floor, then spills.
2183
+ it drains to the floor, then spills;
2184
+ expiring-first does the same in order of
2185
+ each seat's 7-day reset, soonest first.
2181
2186
  Sticky bindings are unaffected.
2182
2187
  Env: DARIO_POOL_STRATEGY.
2183
2188
  --pool-headroom-floor=<ratio|percent>
@@ -86,8 +86,10 @@ export interface DarioConfig {
86
86
  * most headroom; `fill-first` concentrates them on the alphabetically-
87
87
  * first eligible seat until it drains to the 2% floor, then spills to
88
88
  * the next — primary/backup semantics, alias order is the knob.
89
+ * `expiring-first` is fill-first ordered by each seat's 7-day reset,
90
+ * soonest first.
89
91
  */
90
- strategy?: 'headroom' | 'fill-first';
92
+ strategy?: 'headroom' | 'fill-first' | 'expiring-first';
91
93
  /** Headroom at/below which a seat counts as drained — ratio 0.02..0.5 (dario#1333). */
92
94
  headroomFloor?: number;
93
95
  };
@@ -308,7 +308,8 @@ function sanitize(parsed) {
308
308
  }
309
309
  if (isPlainObject(parsed.pool)) {
310
310
  out.pool = {};
311
- if (parsed.pool.strategy === 'headroom' || parsed.pool.strategy === 'fill-first') {
311
+ if (parsed.pool.strategy === 'headroom' || parsed.pool.strategy === 'fill-first'
312
+ || parsed.pool.strategy === 'expiring-first') {
312
313
  out.pool.strategy = parsed.pool.strategy;
313
314
  }
314
315
  if (typeof parsed.pool.headroomFloor === 'number' && Number.isFinite(parsed.pool.headroomFloor)) {
package/dist/pool.d.ts CHANGED
@@ -33,6 +33,14 @@ export interface RateLimitSnapshot {
33
33
  * were on the response.
34
34
  */
35
35
  perModel7d: Record<string, number>;
36
+ /**
37
+ * Epoch SECONDS the seat's own 7-day window resets at, from
38
+ * `anthropic-ratelimit-unified-7d-reset`; 0 when the response did not carry
39
+ * it. Distinct from `reset`, which is the representative claim's reset (on
40
+ * most responses the five-hour one). `expiring-first` orders seats by it:
41
+ * capacity that expires soonest is spent first.
42
+ */
43
+ reset7d?: number;
36
44
  overageUtil: number;
37
45
  claim: string;
38
46
  reset: number;
@@ -385,7 +393,18 @@ export interface PoolStatus {
385
393
  * pick the fill order. Sticky bindings behave identically in both modes;
386
394
  * strategy only decides where UNBOUND (new) conversations land.
387
395
  */
388
- export type PoolStrategy = 'headroom' | 'fill-first';
396
+ export type PoolStrategy = 'headroom' | 'fill-first' | 'expiring-first';
397
+ /**
398
+ * `expiring-first` — fill-first, ordered by WHEN each seat's capacity expires
399
+ * instead of by alias: new conversations fill the seat whose 7-day window
400
+ * resets soonest until it drains to the floor, then spill to the next-soonest.
401
+ * Subscription capacity is use-it-or-lose-it; spending the soonest-expiring
402
+ * first is what gets the most of it used. Fleet box, 2026-09-22: seat pro1 got
403
+ * a fresh limit reset expiring in ~50h while login's window had ~70h left, and
404
+ * alias-ordered fill-first kept every new conversation on login. A seat with
405
+ * no 7d reading yet (or whose window has already rolled) goes last; ties break
406
+ * by alias. Sticky bindings behave identically.
407
+ */
389
408
  /**
390
409
  * Resolve the pool strategy from an explicit value (CLI flag / config file,
391
410
  * already precedence-merged by the caller) with `DARIO_POOL_STRATEGY` as
@@ -550,6 +569,12 @@ export declare function parsePoolHeadroomFloor(value: string | number | null | u
550
569
  * default, matching `resolvePoolStrategy`.
551
570
  */
552
571
  export declare function resolvePoolHeadroomFloor(explicit?: string | number | null, env?: NodeJS.ProcessEnv): number;
572
+ /**
573
+ * The 7-day reset `expiring-first` orders by, in epoch ms: the seat's own
574
+ * reading while it is still ahead of `now`, else +Infinity (never read, or the
575
+ * window has rolled and the next reset is unknown), which sorts it last.
576
+ */
577
+ export declare function seatExpiry(account: PoolAccount, now?: number): number;
553
578
  export declare class AccountPool {
554
579
  private readonly strategy;
555
580
  /** Headroom at/below which a seat counts as drained — see DEFAULT_POOL_HEADROOM_FLOOR. */
package/dist/pool.js CHANGED
@@ -319,7 +319,15 @@ export function familyParkedOnBuckets(rl, family, now = Date.now()) {
319
319
  }
320
320
  /** `next` with the parked buckets `prev` still holds carried forward (see `RateLimitSnapshot.parkedBuckets`). */
321
321
  export function withParkedBuckets(prev, next, now = Date.now()) {
322
- const parked = activeParkedBuckets(prev, now);
322
+ // A reading that MEASURED a parked bucket below the threshold is that bucket's
323
+ // own answer: it has room again before its stated reset (a limit reset on the
324
+ // account, a plan upgrade), so its park lifts. A reading without the bucket's
325
+ // header (an Opus 200 carries no 7d_oi) says nothing about it and keeps it.
326
+ // Fleet box, 2026-09-22: pro1's oi was reset by the operator; a pinned Fable
327
+ // 200 read 7d_oi 0.0 allowed, and the park still held Fable off pro1 until the
328
+ // 429's reset three days out.
329
+ const parked = activeParkedBuckets(prev, now)
330
+ .filter((b) => !(b in next.perModel7d && next.perModel7d[b] < 0.99));
323
331
  if (parked.length === 0)
324
332
  return next;
325
333
  return { ...next, parkedBuckets: parked, parkedBucketsUntil: prev.parkedBucketsUntil };
@@ -392,6 +400,17 @@ export function accountAction(account, now = Date.now()) {
392
400
  return 'wait';
393
401
  return 'none';
394
402
  }
403
+ /**
404
+ * `expiring-first` — fill-first, ordered by WHEN each seat's capacity expires
405
+ * instead of by alias: new conversations fill the seat whose 7-day window
406
+ * resets soonest until it drains to the floor, then spill to the next-soonest.
407
+ * Subscription capacity is use-it-or-lose-it; spending the soonest-expiring
408
+ * first is what gets the most of it used. Fleet box, 2026-09-22: seat pro1 got
409
+ * a fresh limit reset expiring in ~50h while login's window had ~70h left, and
410
+ * alias-ordered fill-first kept every new conversation on login. A seat with
411
+ * no 7d reading yet (or whose window has already rolled) goes last; ties break
412
+ * by alias. Sticky bindings behave identically.
413
+ */
395
414
  /**
396
415
  * Resolve the pool strategy from an explicit value (CLI flag / config file,
397
416
  * already precedence-merged by the caller) with `DARIO_POOL_STRATEGY` as
@@ -404,7 +423,7 @@ export function resolvePoolStrategy(explicit, env = process.env) {
404
423
  if (typeof c !== 'string')
405
424
  continue;
406
425
  const s = c.trim().toLowerCase();
407
- if (s === 'headroom' || s === 'fill-first')
426
+ if (s === 'headroom' || s === 'fill-first' || s === 'expiring-first')
408
427
  return s;
409
428
  }
410
429
  return 'headroom';
@@ -526,6 +545,7 @@ export function parseRateLimits(headers, family) {
526
545
  status: get('status') || 'unknown',
527
546
  util5h: parseFloat(get('5h-utilization')) || 0,
528
547
  util7d: parseFloat(get('7d-utilization')) || 0,
548
+ reset7d: parseInt(get('7d-reset'), 10) || 0,
529
549
  perModel7d,
530
550
  overageUtil: parseFloat(get('overage-utilization')) || 0,
531
551
  claim: get('representative-claim') || 'unknown',
@@ -789,6 +809,23 @@ function pickMaxHeadroom(accounts, family) {
789
809
  // readdir whose order the OS doesn't guarantee, and the operator can control
790
810
  // alias names but not readdir. Returns null when every candidate is at/below
791
811
  // the floor so the caller can fall back to max-headroom.
812
+ /**
813
+ * The 7-day reset `expiring-first` orders by, in epoch ms: the seat's own
814
+ * reading while it is still ahead of `now`, else +Infinity (never read, or the
815
+ * window has rolled and the next reset is unknown), which sorts it last.
816
+ */
817
+ export function seatExpiry(account, now = Date.now()) {
818
+ const r = (account.rateLimit.reset7d ?? 0) * 1000;
819
+ return r > now ? r : Number.POSITIVE_INFINITY;
820
+ }
821
+ function pickExpiringFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR, now = Date.now()) {
822
+ const order = [...accounts].sort((a, b) => (seatExpiry(a, now) - seatExpiry(b, now)) || (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
823
+ for (const a of order) {
824
+ if (computeHeadroom(a.rateLimit, family) > floor)
825
+ return a;
826
+ }
827
+ return null;
828
+ }
792
829
  function pickFillFirst(accounts, family, floor = DEFAULT_POOL_HEADROOM_FLOOR) {
793
830
  let best = null;
794
831
  for (const a of accounts) {
@@ -924,8 +961,10 @@ export class AccountPool {
924
961
  const all = [...this.accounts.values()];
925
962
  const eligible = all.filter(a => isAccountEligible(a, now, family));
926
963
  if (eligible.length > 0) {
927
- if (this.strategy === 'fill-first') {
928
- const first = pickFillFirst(eligible, family, this.headroomFloor);
964
+ if (this.strategy === 'fill-first' || this.strategy === 'expiring-first') {
965
+ const first = this.strategy === 'fill-first'
966
+ ? pickFillFirst(eligible, family, this.headroomFloor)
967
+ : pickExpiringFirst(eligible, family, this.headroomFloor, now);
929
968
  if (first)
930
969
  return first;
931
970
  // Every eligible account is at/below the floor — the terminal state
@@ -1091,8 +1130,10 @@ export class AccountPool {
1091
1130
  // after a 429 is the next alias in line, not the max-headroom seat —
1092
1131
  // otherwise a single failover would defeat the concentration the
1093
1132
  // strategy exists to provide.
1094
- if (this.strategy === 'fill-first') {
1095
- const first = pickFillFirst(eligible, family, this.headroomFloor);
1133
+ if (this.strategy === 'fill-first' || this.strategy === 'expiring-first') {
1134
+ const first = this.strategy === 'fill-first'
1135
+ ? pickFillFirst(eligible, family, this.headroomFloor)
1136
+ : pickExpiringFirst(eligible, family, this.headroomFloor, now);
1096
1137
  if (first)
1097
1138
  return first;
1098
1139
  }
@@ -1112,7 +1153,12 @@ export class AccountPool {
1112
1153
  const account = this.accounts.get(alias);
1113
1154
  if (!account)
1114
1155
  return;
1115
- account.rateLimit = withParkedBuckets(account.rateLimit, withBoundBuckets(account.rateLimit, snapshot), snapshot.updatedAt || Date.now());
1156
+ const at = snapshot.updatedAt || Date.now();
1157
+ // A reading without the 7d-reset header does not un-learn it while it is
1158
+ // still ahead: expiring-first would otherwise send the seat to the back.
1159
+ const prevReset7d = account.rateLimit.reset7d ?? 0;
1160
+ const next = !snapshot.reset7d && prevReset7d * 1000 > at ? { ...snapshot, reset7d: prevReset7d } : snapshot;
1161
+ account.rateLimit = withParkedBuckets(account.rateLimit, withBoundBuckets(account.rateLimit, next), at);
1116
1162
  account.adoptedFrom = undefined;
1117
1163
  account.requestCount++;
1118
1164
  }
package/dist/proxy.js CHANGED
@@ -1522,7 +1522,8 @@ export async function startProxy(opts = {}) {
1522
1522
  console.error('[dario] --pool-shared-state needs DARIO_REFRESH_LOCK_URL (the lock service carries the shared state) — running with this instance\'s own state');
1523
1523
  }
1524
1524
  if (poolStrategy !== 'headroom') {
1525
- console.log(` Pool strategy: ${poolStrategy} (new conversations fill the alphabetically-first seat, spill at the 2% floor)`);
1525
+ const order = poolStrategy === 'expiring-first' ? 'the seat whose 7-day window resets soonest' : 'the alphabetically-first seat';
1526
+ console.log(` Pool strategy: ${poolStrategy} (new conversations fill ${order}, spill at the ${Math.round(pool.headroomFloor * 100)}% floor)`);
1526
1527
  }
1527
1528
  if (poolSync) {
1528
1529
  console.log(` Pool shared state: on (instance ${poolSync.instance}, via ${lockUrl}, pulling peers every ${poolSync.intervalMs}ms; fails open)`);
@@ -38,7 +38,7 @@ const FIELDS = [
38
38
  { path: 'thinkTime.maxMs', label: 'Think-time cap (ms)', type: 'number', hint: 'upper bound for the whole formula' },
39
39
  { path: 'sessionStart.minMs', label: 'Session-start min', type: 'number', hint: 'first-request delay floor' },
40
40
  { path: 'sessionStart.jitterMs', label: 'Session-start jitter', type: 'number' },
41
- { path: 'pool.strategy', label: 'Pool strategy', type: 'string', hint: '"headroom" (default) or "fill-first"' },
41
+ { path: 'pool.strategy', label: 'Pool strategy', type: 'string', hint: '"headroom" (default), "expiring-first" or "fill-first"' },
42
42
  // ── Overage-guard (v4.1, dario#288) ─────────────────────────
43
43
  { path: 'overageGuard.enabled', label: 'Overage-guard', type: 'bool', hint: 'halt proxy on any representative-claim=overage' },
44
44
  { path: 'overageGuard.behavior', label: 'Overage behavior', type: 'string', hint: '"halt" (default) or "warn"' },
@@ -271,7 +271,7 @@ function commitEdit(state) {
271
271
  */
272
272
  const STRING_ENUMS = {
273
273
  'overageGuard.behavior': ['halt', 'warn'],
274
- 'pool.strategy': ['headroom', 'fill-first'],
274
+ 'pool.strategy': ['headroom', 'fill-first', 'expiring-first'],
275
275
  };
276
276
  function doSave(state) {
277
277
  try {
package/docs/commands.md CHANGED
@@ -51,7 +51,7 @@ This page is the per-flag reference. For environment variables grouped by task
51
51
  | `--session-rotate-jitter=<ms>` / `DARIO_SESSION_JITTER_MS` | Jitter sampled once per session at creation — hides the exact idle floor. (v3.28) | `0` |
52
52
  | `--session-max-age=<ms>` / `DARIO_SESSION_MAX_AGE_MS` | Hard ceiling on a session-id's lifetime regardless of activity. (v3.28) | off |
53
53
  | `--session-per-client` / `DARIO_SESSION_PER_CLIENT=1` | Split session-id registry by a per-client header so multi-UI fan-out doesn't collapse onto one id. (v3.28) | off |
54
- | `--pool-strategy=<headroom\|fill-first>` / `DARIO_POOL_STRATEGY` | Where new conversations land in a multi-account pool. `headroom` spreads them to the seat with the most slack; `fill-first` concentrates them on the alphabetically-first eligible seat until it drains to the 2% floor, then spills to the next — primary/backup semantics, alias naming (`1-main`, `2-overflow`) picks the fill order. Sticky bindings behave identically under both. See [Multi-account pool](./multi-account-pool.md#routing-strategy). | `headroom` |
54
+ | `--pool-strategy=<headroom\|fill-first\|expiring-first>` / `DARIO_POOL_STRATEGY` | Where new conversations land in a multi-account pool. `headroom` spreads them to the seat with the most slack; `fill-first` concentrates them on the alphabetically-first eligible seat until it drains to the floor, then spills to the next — primary/backup semantics, alias naming (`1-main`, `2-overflow`) picks the fill order; `expiring-first` does the same in order of each seat's 7-day reset, soonest first, so capacity that expires soonest is spent first. Sticky bindings behave identically under all three. See [Multi-account pool](./multi-account-pool.md#routing-strategy). | `headroom` |
55
55
  | `--pool-fallback=<models>` / `DARIO_POOL_FALLBACK` / config `poolFallback.model` | Strictly opt-in. When every pool seat is drained or in auth cool-down (at selection, or mid-flight on a 429 with no peer left), serve the request as `<model>` from whichever provider can, instead of surfacing the 429/503. Accepts a **chain** — `gpt-5.6-sol,claude-sonnet-5` — read left to right, each provider taking the first entry it can serve. A Codex/ChatGPT subscription that lists the model is preferred (no per-token cost) and works on **both** wire shapes; otherwise a configured openai-compat backend, which is still OpenAI-path only (no Messages translation on that route). A chain also makes failover **symmetric**: a rate-limited or failing subscription hands the request back to the Claude pool. Only a 429/5xx fails over — a 400 surfaces, since a bad request would just reproduce itself elsewhere. Every substituted response carries `x-dario-pool-fallback: <model>` — never silent. Needs a Codex account (`dario add altman`) or a backend (`dario backend add …`); `dario doctor` reports it as INERT with neither. Empty pool still 503s (setup error, not traffic to re-bill). Empty flag value disables, overriding env + config. See [Pool-exhausted fallback](./multi-account-pool.md#pool-exhausted-fallback). | off |
56
56
  | `--system-prompt=<verbatim\|partial\|aggressive\|filepath>` / `DARIO_SYSTEM_PROMPT` | System-prompt mode for outbound CC-shaped requests. `partial` strips behavioral constraints (Tone-and-style, Text-output, scope/verbosity/comment bullets) for ~1.2–2.8× output capability on open-ended work. `aggressive` adds prompt-level RLHF restatement removal (<3% over partial — alignment is RLHF-trained). `<filepath>` fully replaces the slot with file contents. Empirically validated as unfingerprinted by the billing classifier — see [`system-prompt.md`](./system-prompt.md) and [`research/system-prompt-classifier-study.md`](./research/system-prompt-classifier-study.md). (v3.34) | `verbatim` |
57
57
  | `--upstream-proxy=<url>` / `--via=<url>` / `DARIO_UPSTREAM_PROXY` | Route dario's outbound fetches (api.anthropic.com, OpenAI-compat backends, OAuth) through an HTTP/HTTPS proxy. Pair with the HTTP proxy mode of a VPN provider (Mullvad, AirVPN), a corporate proxy, privoxy/Tor, etc. Localhost calls bypass. Requires Bun runtime; SOCKS5 not supported. Full provider matrix + setup in [`vpn-routing.md`](./vpn-routing.md). (v3.35) | unset |
@@ -24,8 +24,8 @@ services:
24
24
  expose: ["3456"]
25
25
 
26
26
  cordon:
27
- # multi-arch, Sigstore-attested: gh attestation verify oci://ghcr.io/askalf/cordon:v0.2.0 --repo askalf/cordon
28
- image: ghcr.io/askalf/cordon:v0.2.0
27
+ # multi-arch, Sigstore-attested: gh attestation verify oci://ghcr.io/askalf/cordon:v0.3.0 --repo askalf/cordon
28
+ image: ghcr.io/askalf/cordon:v0.3.0
29
29
  environment:
30
30
  ANTHROPIC_BASE: http://dario:3456
31
31
  OPENAI_BASE: http://dario:3456
@@ -29,7 +29,9 @@ Two situations where that beats spreading:
29
29
  - **Primary/backup seats.** A `z-backup` account stays completely untouched — fresh 5h and 7d windows — until `a-main` is actually drained. Headroom spreading would nibble at both from the first request.
30
30
  - **Cache concentration.** Every fresh conversation lands where the prompt-cache pressure already is, so the spill seat's windows are fully fresh when the primary hits its wall.
31
31
 
32
- Alias order is the operator's knob: name seats `1-main` / `2-overflow` to pick the fill order. Strategy only decides where **unbound** conversations land — sticky bindings (below) behave identically in both modes, and a conversation bound to a seat stays there until that seat is rejected, expiring, or under the floor.
32
+ Alias order is the operator's knob: name seats `1-main` / `2-overflow` to pick the fill order. Strategy only decides where **unbound** conversations land — sticky bindings (below) behave identically in every mode, and a conversation bound to a seat stays there until that seat is rejected, expiring, or under the floor.
33
+
34
+ `--pool-strategy=expiring-first` is fill-first ordered by **when each seat's capacity expires** instead of by alias: new conversations fill the seat whose 7-day window resets soonest (`anthropic-ratelimit-unified-7d-reset`) until it drains to the floor, then spill to the next-soonest; failover follows the same order. Subscription capacity is use-it-or-lose-it, so this is the order that gets the most of it used — the case it was built for is a seat that just got a limit reset, or joined the pool mid-week, sitting beside one whose window has days left. A seat with no 7-day reading yet, or whose window has already rolled, goes last; ties break by alias, so a pool with no readings behaves exactly like fill-first.
33
35
 
34
36
  ## Session stickiness
35
37
 
@@ -21,7 +21,7 @@ Three things it does that a round-robin doesn't:
21
21
 
22
22
  <img src="../.github/readme/pool.jpg" alt="Three pooled seats, work, personal and side, each with a headroom bar. dario routes the request to the seat with the most headroom." width="100%">
23
23
 
24
- `--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups. `--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).
24
+ `--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).
25
25
 
26
26
  ## One key per developer
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.10.5",
3
+ "version": "6.11.0",
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": {