@askalf/dario 6.0.38 → 6.0.39

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/proxy.js CHANGED
@@ -12,7 +12,8 @@ import { darioVersion } from './version.js';
12
12
  import { buildCCRequest, applyCcPromptCaching, isGenuineCCClient, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, overlayTemplateHeaderValues, forwardClientCCIdentityHeaders, isMcpToolName, CC_TEMPLATE, effectiveCacheControl, withForced1hBeta } from './cc-template.js';
13
13
  import { stampCch, hasCchSeed } from './cch.js';
14
14
  import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion } from './live-fingerprint.js';
15
- import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness, rateLimitWindow, describeRateLimitSnapshot, windowPeers, distinctWindows, accountAction } from './pool.js';
15
+ import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, reconcilePoolAccounts, resolvePoolStrategy, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail } from './pool.js';
16
+ import { backfillIdentity } from './accounts.js';
16
17
  import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
17
18
  import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
18
19
  import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
@@ -1330,22 +1331,23 @@ export async function startProxy(opts = {}) {
1330
1331
  const accountsList = await loadAllAccounts();
1331
1332
  const poolStrategy = resolvePoolStrategy(opts.poolStrategy);
1332
1333
  const pool = new AccountPool(poolStrategy);
1333
- // Two aliases reporting one window are one subscription counted twice
1334
- // (dario#1244). Said once per pair, when the second reading arrives; the
1335
- // listings carry it permanently as `sharesWindowWith`.
1336
- const announcedWindowPairs = new Set();
1337
- const announceWindowPeers = (alias) => {
1338
- const now = Date.now();
1339
- const seat = pool.get(alias);
1340
- if (!seat)
1341
- return;
1342
- for (const peer of windowPeers(pool.all(), now).get(alias) ?? []) {
1343
- const pair = [alias, peer].sort().join('|');
1344
- if (announcedWindowPairs.has(pair))
1345
- continue;
1346
- announcedWindowPairs.add(pair);
1347
- const windows = distinctWindows(pool.all(), now);
1348
- console.error(`[dario] seats "${alias}" and "${peer}" report the same ${seat.rateLimit.claim} window (resets ${new Date(seat.rateLimit.reset * 1000).toISOString()}) — one subscription under two aliases; the pool has ${windows} distinct window${windows === 1 ? '' : 's'} across ${pool.size} seats`);
1334
+ // Two aliases that are one account (same OAuth account uuid) are one
1335
+ // subscription counted twice (dario#1244). Said once per pair, from what the
1336
+ // records know at load and after any reconcile; the listings carry it
1337
+ // permanently as `sameAccountAs` / `sharesWindowWith`. Identity, not the
1338
+ // reset-second inference this replaced (dario#1263).
1339
+ const announcedAccountPairs = new Set();
1340
+ const announceSameAccounts = () => {
1341
+ const peers = accountPeers(pool.all());
1342
+ for (const seat of pool.all()) {
1343
+ for (const peer of peers.get(seat.alias) ?? []) {
1344
+ const pair = [seat.alias, peer].sort().join('|');
1345
+ if (announcedAccountPairs.has(pair))
1346
+ continue;
1347
+ announcedAccountPairs.add(pair);
1348
+ const who = seat.accountEmail ? ` (${maskEmail(seat.accountEmail)})` : '';
1349
+ console.error(`[dario] seats "${seat.alias}" and "${peer}" are the same account${who} — one subscription under two aliases; ${distinctAccounts(pool.all())} distinct accounts across ${pool.size} seats`);
1350
+ }
1349
1351
  }
1350
1352
  };
1351
1353
  // Shared pool state across instances (docs/multi-instance.md): opt-in,
@@ -1437,8 +1439,13 @@ export async function startProxy(opts = {}) {
1437
1439
  accountUuid: acc.accountUuid,
1438
1440
  grantedAt: acc.grantedAt,
1439
1441
  organizationId: acc.organizationId,
1442
+ accountId: acc.accountId,
1443
+ accountEmail: acc.accountEmail,
1444
+ rateLimitTier: acc.rateLimitTier,
1445
+ seatTier: acc.seatTier,
1440
1446
  });
1441
1447
  }
1448
+ announceSameAccounts();
1442
1449
  // Startup self-heal (dario#790): eagerly refresh any account whose access
1443
1450
  // token is already expired or within the 45-min refresh window BEFORE the
1444
1451
  // proxy starts serving. On a container recreate after >8h uptime the
@@ -1461,6 +1468,22 @@ export async function startProxy(opts = {}) {
1461
1468
  // observed on (dario#1244) — the one write that touches the record.
1462
1469
  const refreshed = await refreshAccountToken(withObservedOrganization(saved, acc.organizationId));
1463
1470
  pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
1471
+ // Identity back-fill (dario#1263): a record from before accountId learns
1472
+ // who it is on its first refresh under this release — one GET through
1473
+ // this proxy's fetch, never fatal — and the running seat is reconciled so
1474
+ // the listings and the same-account line know at once.
1475
+ if (!refreshed.accountId) {
1476
+ const identified = await backfillIdentity(refreshed, fetch).catch(() => null);
1477
+ if (identified?.accountId) {
1478
+ pool.add(acc.alias, {
1479
+ accessToken: identified.accessToken, refreshToken: identified.refreshToken, expiresAt: identified.expiresAt,
1480
+ deviceId: identified.deviceId, accountUuid: identified.accountUuid, grantedAt: identified.grantedAt,
1481
+ organizationId: identified.organizationId, accountId: identified.accountId, accountEmail: identified.accountEmail,
1482
+ rateLimitTier: identified.rateLimitTier, seatTier: identified.seatTier,
1483
+ });
1484
+ announceSameAccounts();
1485
+ }
1486
+ }
1464
1487
  // Mirror a refreshed `login` token back to credentials.json so the
1465
1488
  // legacy file (and `dario doctor`) tracks the pool store (#808).
1466
1489
  await mirrorLoginToCredentials(refreshed).catch((err) => {
@@ -1515,6 +1538,22 @@ export async function startProxy(opts = {}) {
1515
1538
  // observed on (dario#1244) — the one write that touches the record.
1516
1539
  const refreshed = await refreshAccountToken(withObservedOrganization(saved, acc.organizationId));
1517
1540
  pool.updateTokens(acc.alias, refreshed.accessToken, refreshed.refreshToken, refreshed.expiresAt);
1541
+ // Identity back-fill (dario#1263): a record from before accountId learns
1542
+ // who it is on its first refresh under this release — one GET through
1543
+ // this proxy's fetch, never fatal — and the running seat is reconciled so
1544
+ // the listings and the same-account line know at once.
1545
+ if (!refreshed.accountId) {
1546
+ const identified = await backfillIdentity(refreshed, fetch).catch(() => null);
1547
+ if (identified?.accountId) {
1548
+ pool.add(acc.alias, {
1549
+ accessToken: identified.accessToken, refreshToken: identified.refreshToken, expiresAt: identified.expiresAt,
1550
+ deviceId: identified.deviceId, accountUuid: identified.accountUuid, grantedAt: identified.grantedAt,
1551
+ organizationId: identified.organizationId, accountId: identified.accountId, accountEmail: identified.accountEmail,
1552
+ rateLimitTier: identified.rateLimitTier, seatTier: identified.seatTier,
1553
+ });
1554
+ announceSameAccounts();
1555
+ }
1556
+ }
1518
1557
  // Mirror a refreshed `login` token back to credentials.json so the
1519
1558
  // legacy file (and `dario doctor`) tracks the pool store (#808).
1520
1559
  await mirrorLoginToCredentials(refreshed).catch((err) => {
@@ -1559,7 +1598,12 @@ export async function startProxy(opts = {}) {
1559
1598
  accountUuid: acc.accountUuid,
1560
1599
  grantedAt: acc.grantedAt,
1561
1600
  organizationId: acc.organizationId,
1601
+ accountId: acc.accountId,
1602
+ accountEmail: acc.accountEmail,
1603
+ rateLimitTier: acc.rateLimitTier,
1604
+ seatTier: acc.seatTier,
1562
1605
  });
1606
+ announceSameAccounts();
1563
1607
  }
1564
1608
  }
1565
1609
  }
@@ -2147,7 +2191,7 @@ export async function startProxy(opts = {}) {
2147
2191
  // just persisted metadata — the same snapshot GET /accounts exposes.
2148
2192
  poolStatus: () => {
2149
2193
  const snapNow = Date.now();
2150
- const peers = windowPeers(pool.all(), snapNow);
2194
+ const peers = accountPeers(pool.all());
2151
2195
  const snap = new Map();
2152
2196
  for (const a of pool.all()) {
2153
2197
  snap.set(a.alias, {
@@ -2166,6 +2210,11 @@ export async function startProxy(opts = {}) {
2166
2210
  lastRejectedAt: a.lastRejectedAt ?? null,
2167
2211
  organizationId: a.organizationId ?? null,
2168
2212
  sharesWindowWith: peers.get(a.alias) ?? [],
2213
+ sameAccountAs: peers.get(a.alias) ?? [],
2214
+ accountId: a.accountId ?? null,
2215
+ accountEmail: maskEmail(a.accountEmail),
2216
+ rateLimitTier: a.rateLimitTier ?? null,
2217
+ seatTier: a.seatTier ?? null,
2169
2218
  readingFrom: a.adoptedFrom ?? null,
2170
2219
  // Raw streak, not just the cooldown boolean: a single 401 also
2171
2220
  // shows `auth-cooldown` for 60s, indistinguishable from a
@@ -2238,7 +2287,7 @@ export async function startProxy(opts = {}) {
2238
2287
  // the `dario accounts` CLI, not HTTP.
2239
2288
  if (urlPath === '/accounts' && req.method === 'GET') {
2240
2289
  const now = Date.now();
2241
- const peers = windowPeers(pool.all(), now);
2290
+ const peers = accountPeers(pool.all());
2242
2291
  const accounts = pool.all().map(a => {
2243
2292
  const inCooldown = isInAuthCooldown(a, now);
2244
2293
  const cooldownMs = inCooldown && a.lastAuthFailureAt
@@ -2276,11 +2325,17 @@ export async function startProxy(opts = {}) {
2276
2325
  // parked seat no longer reads as one that was never called.
2277
2326
  rejectedCount: a.rejectedCount,
2278
2327
  lastRejectedAt: a.lastRejectedAt ?? null,
2279
- // Which organization the token belongs to, and which other seats
2280
- // report the same live window one subscription under several
2281
- // aliases (dario#1244).
2328
+ // Which organization the token belongs to, who the token IS (OAuth
2329
+ // account uuid, masked email), and which other seats are the same
2330
+ // account — one subscription under several aliases (dario#1244,
2331
+ // #1263: identity, not a shared reset second).
2282
2332
  organizationId: a.organizationId ?? null,
2283
2333
  sharesWindowWith: peers.get(a.alias) ?? [],
2334
+ sameAccountAs: peers.get(a.alias) ?? [],
2335
+ accountId: a.accountId ?? null,
2336
+ accountEmail: maskEmail(a.accountEmail),
2337
+ rateLimitTier: a.rateLimitTier ?? null,
2338
+ seatTier: a.seatTier ?? null,
2284
2339
  // Whose reading this is: a peer instance's id (shared pool state)
2285
2340
  // or null for this instance's own.
2286
2341
  readingFrom: a.adoptedFrom ?? null,
@@ -2306,9 +2361,11 @@ export async function startProxy(opts = {}) {
2306
2361
  mode: 'pool',
2307
2362
  ...pool.status(),
2308
2363
  stickyBindings: pool.stickyCount(),
2309
- // Windows the pool really has: each measured window once, each
2310
- // unmeasured seat as its own.
2311
- distinctWindows: distinctWindows(pool.all(), now),
2364
+ // Accounts the pool really has: each identified account once, each
2365
+ // not-yet-identified seat as its own. `distinctWindows` keeps its name
2366
+ // for readers of the older payload; an account is its windows.
2367
+ distinctWindows: distinctAccounts(pool.all()),
2368
+ distinctAccounts: distinctAccounts(pool.all()),
2312
2369
  // Shared pool state (pool-sync.ts) — null when off.
2313
2370
  sharedState: poolSync ? poolSync.status() : null,
2314
2371
  accounts,
@@ -3920,7 +3977,9 @@ export async function startProxy(opts = {}) {
3920
3977
  // returns status='rejected' on 429, which makes the next `select()` call
3921
3978
  // route traffic away from this account until it resets.
3922
3979
  if (poolAccount) {
3923
- const snapshot = parseRateLimits(upstream.headers);
3980
+ // The family is what lets a bucket the wire does not name by family
3981
+ // (`7d_oi`) be learned as binding THIS request's model (dario#1262).
3982
+ const snapshot = parseRateLimits(upstream.headers, modelFamily(requestModel));
3924
3983
  if (upstream.status === 429) {
3925
3984
  // Say so the moment a seat leaves rotation. With a peer to fail
3926
3985
  // over to the client sees 200, and nothing else named the seat,
@@ -3929,7 +3988,7 @@ export async function startProxy(opts = {}) {
3929
3988
  // those repeats are verbose-only.
3930
3989
  const parked = pool.markRejected(poolAccount.alias, snapshot);
3931
3990
  if (parked || verbose) {
3932
- console.error(`[dario] #${requestCount} rate limited (429) on account "${poolAccount.alias}": ${describeRateLimitSnapshot(snapshot)} — parked until the window rolls`);
3991
+ console.error(`[dario] #${requestCount} rate limited (429) on account "${poolAccount.alias}": ${describeRejection(pool.get(poolAccount.alias)?.rateLimit ?? snapshot)}`);
3933
3992
  }
3934
3993
  }
3935
3994
  else {
@@ -3941,7 +4000,6 @@ export async function startProxy(opts = {}) {
3941
4000
  const organizationId = upstream.headers.get('anthropic-organization-id');
3942
4001
  if (organizationId)
3943
4002
  pool.noteOrganization(poolAccount.alias, organizationId);
3944
- announceWindowPeers(poolAccount.alias);
3945
4003
  poolSync?.reportSeat(poolAccount.alias);
3946
4004
  // First-sight detector for per-model rate-limit buckets. Anthropic
3947
4005
  // ships these unannounced — e.g. `7d_sonnet-utilization` appeared
package/docs/admin-api.md CHANGED
@@ -104,9 +104,7 @@ rejection lifts), representative `claim` (e.g. `five_hour`), routing
104
104
  `status`, `request_count` (requests served), `rejected_count` /
105
105
  `last_rejected_at` (429s answered — a 429 serves nothing, so it is not a
106
106
  request), `organization_id` (the organization the token belongs to, learned
107
- from its responses and written to the record with its next refresh), `shares_window_with` (aliases whose last
108
- reading names the same live window — one subscription under several aliases,
109
- see [One subscription under two aliases](./multi-account-pool.md#one-subscription-under-two-aliases)),
107
+ from its responses and written to the record with its next refresh), `account_id` / `account_email` (who the token is, from its OAuth profile at grant time; the email masked), `same_account_as` (aliases that are the same account — one subscription under several aliases; `shares_window_with` is the same list under its older name, see [One subscription under two aliases](./multi-account-pool.md#one-subscription-under-two-aliases)), `rate_limit_tier` / `seat_tier` when the profile stated them,
110
108
  and `consecutive_auth_failures`. What each `status` means and what
111
109
  to do about it: [Reading a seat's `status`](./multi-account-pool.md#reading-a-seats-status).
112
110
  It's the admin-token-gated equivalent of the proxy-key-gated `GET /accounts`
package/docs/commands.md CHANGED
@@ -16,6 +16,7 @@ This page is the per-flag reference. For environment variables grouped by task
16
16
  | `dario refresh` | Force an immediate Claude token refresh |
17
17
  | `dario logout` | Delete stored Claude credentials |
18
18
  | `dario accounts check <alias> [--models=a,b]` | Read-only, in-place seat probe: one tiny request per model, pinned to that seat through the running proxy (`x-dario-account` + `x-dario-admin-token`, needs `DARIO_ADMIN=1`). A pinned request never fails over, so the upstream status is the seat's own answer. |
19
+ | `dario accounts identity [--fresh <alias>...\|--all]` | Which client identity each seat presents in `metadata.user_id`, where it came from, and which seats share one across different accounts; `--fresh` gives the named seats their own (the running proxy presents it on the next request). See [Client identity](./multi-account-pool.md#client-identity-what-a-seat-presents-as). |
19
20
  | `dario accounts list` / `add <alias>` / `remove <alias>` | Multi-account pool management. `add <alias>` on a fresh pool auto back-fills your existing `dario login` credentials as `login`, so your first `add` trips the 2+ pool threshold on its own — see [Multi-account pool mode](./multi-account-pool.md). |
20
21
  | `dario backend list` / `add <name> --key=<key> [--base-url=<url>]` / `remove <name>` | OpenAI-compat backend management |
21
22
  | `dario subagent install` / `remove` / `status` | CC sub-agent lifecycle. See [sub-agent hook](./sub-agent.md). |
@@ -98,10 +98,12 @@ curl http://localhost:3456/analytics # per-account / per-model stats, burn ra
98
98
  | `status` | What it means | What to do |
99
99
  |---|---|---|
100
100
  | `allowed` | The seat's last response was a 200 with headroom. `util5h` / `util7d` are that response's reading — a ratio against 1.0, so `0.42` is 42% — `lastObservedAt` / `utilAgeMs` say how old it is, `resetAt` / `resetInMs` when its representative window rolls. | Nothing. |
101
- | `rejected` | The seat's last response was a 429: the organization behind its token is over the window named by `claim` (`five_hour`, `seven_day`, …). `util5h: 1.04` is 104% of the five-hour window, not 1%. `rejectedCount` / `lastRejectedAt` say the seat was tried — a 429 serves nothing, so `requestCount` does not move — and `resetInMs` says how long it stays parked. Requests route around it; it returns on its own when the window rolls. | Nothing — the window clears itself. If the reading surprises you (your usage page for that account says 0%), the token belongs to a different organization than the page you are looking at, or to the same organization as another seat: the reading is Anthropic's own, taken on that token. `dario accounts check <alias>` asks the seat directly. |
101
+ | `rejected` | The seat's last response was a 429 **that named an exhausted window**: `claim` says which (`five_hour`, `seven_day`, …) and the reading is at or past the 1.0 threshold — `util5h: 1.04` is 104% of the five-hour window, not 1%. `rejectedCount` / `lastRejectedAt` say the seat was tried — a 429 serves nothing, so `requestCount` does not move — and `resetInMs` says how long it stays parked. Requests route around it; it returns on its own when the window rolls. | Nothing — the window clears itself. If the reading surprises you (your usage page for that account says 0%), the token belongs to a different organization than the page you are looking at, or to the same organization as another seat: the reading is Anthropic's own, taken on that token. `dario accounts check <alias>` asks the seat directly. |
102
102
  | `unknown` | No current observation: a seat that has served nothing yet, or a rejection whose window has rolled (`resetInMs: 0`) and that nothing has measured since. | Nothing; the next request measures it. |
103
103
  | `auth-cooldown` | Upstream answered 401/403 or `invalid_grant`. `consecutiveAuthFailures` tells a blip (1) from a dead refresh token (a streak); the cool-down doubles with the streak, from 1 minute to 30. | A streak means re-grant the seat — `dario accounts remove` + `add`, or the admin login flow under the same alias. A new grant starts the seat fresh: no carried-over cool-down, rejection or identity. See [Refresh-token grant age](#refresh-token-grant-age) for the 28-day wall behind most streaks. |
104
104
 
105
+ **A 429 that names no exhausted window is not a parking.** A rejection whose headers show no claim, or a claim with utilization nowhere near 1.0 (`5h 0%, 7d 0%, claim unknown`), is a refusal of some other kind — concurrency, an account-level lock, a monthly credit — and the `reset` it states is not this seat's window rolling. Until 6.0.39 the status code alone decided, and a seat on the fleet box was parked for 546 hours on exactly that reading. Such a seat now cools for the response's own `retry-after` (or one minute) and stays probeable; the log says so: `429 without an exhausted window (5h 0%, 7d 0%, claim unknown; stated reset in 546h 49m not honoured) — cooling 1m, seat stays probeable`.
106
+
105
107
  **When every seat is parked.** A pool whose seats are all `rejected` inside live windows does not probe them again: dario answers the request itself with `429`, `retry-after` set to the earliest reset, `x-dario-upstream-rejection: pool_parked`, and nothing sent upstream. One log line marks the transition (`pool parked: all 6 seats are over their rate-limit windows, earliest resets in 21m`). Before 6.0.35 every such request re-probed the earliest-reset seat, so `rejectedCount` on that seat grew by one per request — a seat reading `rejected_count: 500` next to `request_count: 1` was that, not a seat that needed a re-login. With a `--pool-fallback` armed, the request goes to the fallback instead, as before.
106
108
 
107
109
  The proxy logs every parking as it happens, once per window: `rate limited (429) on account "spare": 5h 104%, 7d 25%, claim five_hour, resets in 37m — parked until the window rolls`. The re-probes the all-exhausted fallback makes of an already-parked seat are logged only under `-v`.
@@ -110,16 +112,33 @@ The proxy logs every parking as it happens, once per window: `rate limited (429)
110
112
 
111
113
  ## One subscription under two aliases
112
114
 
113
- A pool of six is only six windows if the six tokens belong to six subscriptions. Two aliases granted from the same account or from two accounts on one organization that share a plan share one five-hour and one seven-day window, and the pool counts that window twice: both seats look like headroom, the busier one fills the window for both, and the other 429s on its first request with the same reading (the #1244 report).
115
+ A pool of six is only six accounts if the six tokens belong to six accounts. Two aliases granted from the same account share one set of windows and one set of limits the pool routes on real headroom either way, and the duplicate simply parks on the first 429 until the window rolls, but the operator should know.
116
+
117
+ dario knows it from the token itself. At grant time (`dario accounts add`, the admin login, the keychain import, the `login` back-fill) it reads the token's OAuth profile and records the **account uuid** — plus the masked email and the organization's tier fields — on the seat's record. A record written before this exists is filled in on its next token refresh.
118
+
119
+ - **`accountId`** / **`accountEmail`** — who the token is. Two seats with the same `accountId` are the same account, full stop.
120
+ - **`sameAccountAs`** (and `sharesWindowWith`, the same list under its original name) — the other aliases that are this account.
121
+ - **`organizationId`** — the `anthropic-organization-id` the seat's responses carry. Several accounts can share an organization (a Team) and still have their own windows: *organization* is not *account*.
122
+
123
+ Until 6.0.38, `sharesWindowWith` was **inferred**: two seats whose last readings named the same window (`claim@reset`) were called one subscription, on the assumption that independent windows never share a reset second. They do — Anthropic aligns the five-hour reset to a 20-minute grid, so a window has 15 possible reset seconds and a pool of 18 seats collides by pigeonhole. That inference told an operator seven independent colleagues were one subscription (dario#1263). It is gone; nothing is claimed that the token did not say.
124
+
125
+ Both facts are on `GET /accounts` (`distinctAccounts` — and `distinctWindows`, kept for readers of the older payload — count the accounts the pool really has), on `GET /admin/accounts` as `account_id` / `account_email` / `same_account_as`, in `dario accounts list --live`, and on the `Accounts` row of `dario doctor`. The proxy also says it once per pair at start-up: `seats "busy" and "twin" are the same account (ma***@example.com)`.
114
126
 
115
- Two facts make this visible:
127
+ ## Client identity: what a seat presents as
116
128
 
117
- - **`organizationId`** the `anthropic-organization-id` every response carries, learned the first time a seat serves and written to its record with the seat's next token refresh. It is what to compare with the organization behind the usage page you are looking at: a reading that surprises you is usually a token on a different organization.
118
- - **`sharesWindowWith`** — the other aliases whose last reading names the same live window (same representative claim, same reset second). Two independent windows all but never share a reset second; two readings of one window always do. This is the fact that matters for headroom, and it is deliberately not derived from the organization: seats on one organization can still have their own windows.
129
+ Every request carries `metadata.user_id` a client identity (`device_id`, `account_uuid`) that Claude Code derives from its install, and that Anthropic ties the bearer token to. dario stores one per seat.
119
130
 
120
- Both are on `GET /accounts` (`distinctWindows` at the top counts the windows the pool really has), on `GET /admin/accounts` as `organization_id` / `shares_window_with`, in `dario accounts list --live`, and in `dario doctor` (the `Organizations` row, from the ids on the records — so up to one refresh behind the running proxy). The proxy also says it once, when the second reading arrives: `seats "twin" and "busy" report the same five_hour window (resets 2026-09-07T13:12:00.000Z) one subscription under two aliases; the pool has 2 distinct windows across 3 seats`.
131
+ Before 6.0.39 every add path copied the **machine's** Claude Code identity into every alias when Claude Code was installed. On a machine running a pool of colleagues' tokens that meant eighteen different accounts all presenting one identity (dario#1244). Whether that alone changes what Anthropic counts is not established it is hygiene, not a diagnosisbut a seat should present the account it belongs to.
132
+
133
+ Now a new alias takes the local Claude Code identity only when no other alias holds it, or when the holder is proven (same `accountId`) to be the same account; otherwise the alias gets its own, exactly as a machine without Claude Code always did. Existing seats are not rewritten behind your back. To see what each seat presents, and which seats share one identity across different accounts:
134
+
135
+ ```
136
+ dario accounts identity # per-seat report
137
+ dario accounts identity --fresh <alias>... # give these seats their own
138
+ dario accounts identity --fresh --all
139
+ ```
121
140
 
122
- What to do about it: nothing is broken — the pool routes on real headroom either way, and the duplicate seat simply parks on the first 429 until the window rolls. If the second alias was meant to be a second subscription, re-grant it while signed in to the right account.
141
+ The running proxy presents a rewritten identity on the seat's next request; no restart. `dario doctor` warns on the `Client identity` row when seats share one across different accounts.
123
142
 
124
143
  ## Consumers: who a request is for
125
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.0.38",
3
+ "version": "6.0.39",
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": {