@askalf/dario 5.2.7 → 5.2.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/doctor.js +20 -0
- package/dist/health-response.d.ts +12 -0
- package/dist/health-response.js +1 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.js +19 -11
- package/dist/proxy.js +9 -1
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -805,6 +805,26 @@ export async function runChecks(opts = {}) {
|
|
|
805
805
|
catch (err) {
|
|
806
806
|
checks.push({ status: 'warn', label: 'Pool', detail: `check failed: ${err.message}` });
|
|
807
807
|
}
|
|
808
|
+
// ---- Live session state (only observable from a running proxy)
|
|
809
|
+
// Session/sticky counts live in the proxy's memory, so — unlike the
|
|
810
|
+
// local-state checks above — this reads them off /health (internal-disclosure
|
|
811
|
+
// caller, so it must reach dario on loopback). Silent skip when no proxy is
|
|
812
|
+
// up: the counts don't exist, and doctor is often run because it's down.
|
|
813
|
+
try {
|
|
814
|
+
const darioBase = process.env.DARIO_TEST_URL || 'http://127.0.0.1:3456';
|
|
815
|
+
const healthRes = await fetch(`${darioBase}/health`, { signal: AbortSignal.timeout(800) });
|
|
816
|
+
if (healthRes.ok) {
|
|
817
|
+
const health = (await healthRes.json().catch(() => null));
|
|
818
|
+
const sx = health?.sessions;
|
|
819
|
+
if (sx && typeof sx.mode === 'string') {
|
|
820
|
+
const detail = sx.mode === 'pool'
|
|
821
|
+
? `${sx.stickyBindings ?? 0} sticky binding${sx.stickyBindings === 1 ? '' : 's'} — conversation→account affinity, lazily reaped (6h idle TTL, cap 2000)`
|
|
822
|
+
: `${sx.active ?? 0} active session id${sx.active === 1 ? '' : 's'} — lazily reaped (LRU cap 1024, no background sweeper)`;
|
|
823
|
+
checks.push({ status: 'info', label: 'Sessions', detail });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
catch { /* proxy not running — live session state unavailable, skip */ }
|
|
808
828
|
// ---- Identity drift (pool account snapshot vs live ~/.claude.json)
|
|
809
829
|
try {
|
|
810
830
|
const { detectClaudeIdentity, listAccountAliases, loadAllAccounts } = await import('./accounts.js');
|
|
@@ -22,6 +22,18 @@ export interface HealthStatusLike {
|
|
|
22
22
|
lastRefreshError?: string;
|
|
23
23
|
/** dario's package version, surfaced to internal callers on /health (#640). */
|
|
24
24
|
version?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Live session-tracking counts, surfaced to internal callers only. Both
|
|
27
|
+
* structures reap lazily (no background sweeper — see session-rotation.ts),
|
|
28
|
+
* so this raw in-memory size also reveals whether lazy cleanup keeps up.
|
|
29
|
+
*/
|
|
30
|
+
sessions?: {
|
|
31
|
+
mode: 'pool';
|
|
32
|
+
stickyBindings: number;
|
|
33
|
+
} | {
|
|
34
|
+
mode: 'single';
|
|
35
|
+
active: number;
|
|
36
|
+
};
|
|
25
37
|
}
|
|
26
38
|
export interface HealthResponse {
|
|
27
39
|
httpStatus: number;
|
package/dist/health-response.js
CHANGED
|
@@ -81,6 +81,7 @@ export function buildHealthResponse(s, requestCount, includeInternal) {
|
|
|
81
81
|
oauth: s.status,
|
|
82
82
|
expiresIn: s.expiresIn,
|
|
83
83
|
requests: requestCount,
|
|
84
|
+
...(s.sessions ? { sessions: s.sessions } : {}),
|
|
84
85
|
...(s.refreshFailures ? { refreshFailures: s.refreshFailures } : {}),
|
|
85
86
|
}
|
|
86
87
|
: liveness;
|
|
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
|
|
|
282
282
|
*/
|
|
283
283
|
export declare const SUPPORTED_CC_RANGE: {
|
|
284
284
|
readonly min: "1.0.0";
|
|
285
|
-
readonly maxTested: "2.1.
|
|
285
|
+
readonly maxTested: "2.1.214";
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
288
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -806,7 +806,7 @@ export function _resetInstalledVersionProbeForTest() {
|
|
|
806
806
|
*/
|
|
807
807
|
export const SUPPORTED_CC_RANGE = {
|
|
808
808
|
min: '1.0.0',
|
|
809
|
-
maxTested: '2.1.
|
|
809
|
+
maxTested: '2.1.214',
|
|
810
810
|
};
|
|
811
811
|
/**
|
|
812
812
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/pool.d.ts
CHANGED
|
@@ -192,7 +192,7 @@ export declare class AccountPool {
|
|
|
192
192
|
*
|
|
193
193
|
* Also performs lazy cleanup of expired bindings (TTL or size cap).
|
|
194
194
|
*/
|
|
195
|
-
selectSticky(stickyKey: string | null, family?: string | null): PoolAccount | null;
|
|
195
|
+
selectSticky(stickyKey: string | null, family?: string | null, now?: number): PoolAccount | null;
|
|
196
196
|
/**
|
|
197
197
|
* Rebind a sticky key to a different account — called by proxy after an
|
|
198
198
|
* in-request 429 failover moves to the next-best account. Without this
|
package/dist/pool.js
CHANGED
|
@@ -157,7 +157,7 @@ export function computeHeadroom(snapshot, family) {
|
|
|
157
157
|
}
|
|
158
158
|
return 1 - Math.max(...utils);
|
|
159
159
|
}
|
|
160
|
-
const
|
|
160
|
+
const STICKY_IDLE_TTL_MS = 6 * 60 * 60 * 1000; // reap a binding 6h after its LAST use, not its creation
|
|
161
161
|
const STICKY_MAX_ENTRIES = 2_000; // lazy cleanup cap
|
|
162
162
|
const STICKY_CLEANUP_INTERVAL_MS = 30_000; // amortize the O(n) TTL/orphan sweep
|
|
163
163
|
/**
|
|
@@ -334,25 +334,28 @@ export class AccountPool {
|
|
|
334
334
|
*
|
|
335
335
|
* Also performs lazy cleanup of expired bindings (TTL or size cap).
|
|
336
336
|
*/
|
|
337
|
-
selectSticky(stickyKey, family) {
|
|
337
|
+
selectSticky(stickyKey, family, now = Date.now()) {
|
|
338
338
|
if (!stickyKey)
|
|
339
339
|
return this.select(family);
|
|
340
|
-
this.cleanupSticky();
|
|
340
|
+
this.cleanupSticky(now);
|
|
341
341
|
const binding = this.sticky.get(stickyKey);
|
|
342
342
|
if (binding) {
|
|
343
343
|
const bound = this.accounts.get(binding.alias);
|
|
344
|
-
const now = Date.now();
|
|
345
344
|
if (bound
|
|
346
345
|
&& bound.rateLimit.status !== 'rejected'
|
|
347
346
|
&& bound.expiresAt > now + 30_000
|
|
348
347
|
&& !isInAuthCooldown(bound, now)
|
|
349
348
|
&& computeHeadroom(bound.rateLimit, family) > POOL_HEADROOM_FLOOR) {
|
|
349
|
+
// Refresh the idle timer. A session that keeps taking turns must never
|
|
350
|
+
// be reaped or rebound while active — that would strand its warm prompt
|
|
351
|
+
// cache — so the TTL is re-based to now on every hit.
|
|
352
|
+
binding.lastUsedAt = now;
|
|
350
353
|
return bound;
|
|
351
354
|
}
|
|
352
355
|
}
|
|
353
356
|
const picked = this.select(family);
|
|
354
357
|
if (picked) {
|
|
355
|
-
this.sticky.set(stickyKey, { alias: picked.alias, boundAt:
|
|
358
|
+
this.sticky.set(stickyKey, { alias: picked.alias, boundAt: now, lastUsedAt: now });
|
|
356
359
|
}
|
|
357
360
|
return picked;
|
|
358
361
|
}
|
|
@@ -367,7 +370,8 @@ export class AccountPool {
|
|
|
367
370
|
return;
|
|
368
371
|
if (!this.accounts.has(alias))
|
|
369
372
|
return;
|
|
370
|
-
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
this.sticky.set(stickyKey, { alias, boundAt: now, lastUsedAt: now });
|
|
371
375
|
}
|
|
372
376
|
/**
|
|
373
377
|
* Drop any binding that points at an account no longer in the pool, any
|
|
@@ -375,8 +379,7 @@ export class AccountPool {
|
|
|
375
379
|
* entries until we're back under. O(n) but n is small (capped at 2k)
|
|
376
380
|
* and this only runs on selectSticky, not on every method.
|
|
377
381
|
*/
|
|
378
|
-
cleanupSticky() {
|
|
379
|
-
const now = Date.now();
|
|
382
|
+
cleanupSticky(now = Date.now()) {
|
|
380
383
|
// TTL/orphan sweep is O(n); amortize it — run at most once per
|
|
381
384
|
// STICKY_CLEANUP_INTERVAL_MS instead of on every selectSticky (#642-audit).
|
|
382
385
|
// Stale bindings are never wrongly USED meanwhile: selectSticky re-validates
|
|
@@ -384,17 +387,22 @@ export class AccountPool {
|
|
|
384
387
|
if (now - this.lastStickyCleanup >= STICKY_CLEANUP_INTERVAL_MS) {
|
|
385
388
|
this.lastStickyCleanup = now;
|
|
386
389
|
for (const [key, b] of this.sticky) {
|
|
387
|
-
|
|
390
|
+
// Reap orphans (account gone) and bindings idle past the TTL. Idle is
|
|
391
|
+
// measured from lastUsedAt, which selectSticky refreshes every turn, so
|
|
392
|
+
// an actively-running conversation is never reaped here.
|
|
393
|
+
if (!this.accounts.has(b.alias) || now - b.lastUsedAt > STICKY_IDLE_TTL_MS) {
|
|
388
394
|
this.sticky.delete(key);
|
|
389
395
|
}
|
|
390
396
|
}
|
|
391
397
|
}
|
|
392
398
|
// Hard size cap always enforced (bounds memory). Batch-evict down to 80% so
|
|
393
399
|
// the O(n log n) sort amortizes over many inserts rather than firing on every
|
|
394
|
-
// new conversation at the cap (#642-audit).
|
|
400
|
+
// new conversation at the cap (#642-audit). Evict least-recently-USED first
|
|
401
|
+
// (true LRU): a binding's only value is its warm prompt cache, and the ones
|
|
402
|
+
// untouched longest are the coldest — least worth keeping.
|
|
395
403
|
if (this.sticky.size > STICKY_MAX_ENTRIES) {
|
|
396
404
|
const target = Math.floor(STICKY_MAX_ENTRIES * 0.8);
|
|
397
|
-
const sorted = [...this.sticky.entries()].sort((a, b) => a[1].
|
|
405
|
+
const sorted = [...this.sticky.entries()].sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
|
|
398
406
|
const toDrop = sorted.slice(0, this.sticky.size - target);
|
|
399
407
|
for (const [key] of toDrop)
|
|
400
408
|
this.sticky.delete(key);
|
package/dist/proxy.js
CHANGED
|
@@ -1643,7 +1643,15 @@ export async function startProxy(opts = {}) {
|
|
|
1643
1643
|
loopback: isLoopbackAddr(req.socket?.remoteAddress),
|
|
1644
1644
|
viaCfRay: req.headers['cf-ray'] !== undefined,
|
|
1645
1645
|
});
|
|
1646
|
-
const { httpStatus, body } = buildHealthResponse({
|
|
1646
|
+
const { httpStatus, body } = buildHealthResponse({
|
|
1647
|
+
...s,
|
|
1648
|
+
version: darioVersion(),
|
|
1649
|
+
// pool.size === 0 is single-account mode (session-id registry drives
|
|
1650
|
+
// the SESSION_ID slot); a loaded pool routes via sticky bindings.
|
|
1651
|
+
sessions: pool.size === 0
|
|
1652
|
+
? { mode: 'single', active: sessionRegistry.size() }
|
|
1653
|
+
: { mode: 'pool', stickyBindings: pool.stickyCount() },
|
|
1654
|
+
}, requestCount, includeInternal);
|
|
1647
1655
|
res.writeHead(httpStatus, JSON_HEADERS);
|
|
1648
1656
|
res.end(JSON.stringify(body));
|
|
1649
1657
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.8",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|