@askalf/dario
Advanced tools
+20
-0
@@ -808,2 +808,22 @@ /** | ||
| } | ||
| // ---- Live session state (only observable from a running proxy) | ||
| // Session/sticky counts live in the proxy's memory, so — unlike the | ||
| // local-state checks above — this reads them off /health (internal-disclosure | ||
| // caller, so it must reach dario on loopback). Silent skip when no proxy is | ||
| // up: the counts don't exist, and doctor is often run because it's down. | ||
| try { | ||
| const darioBase = process.env.DARIO_TEST_URL || 'http://127.0.0.1:3456'; | ||
| const healthRes = await fetch(`${darioBase}/health`, { signal: AbortSignal.timeout(800) }); | ||
| if (healthRes.ok) { | ||
| const health = (await healthRes.json().catch(() => null)); | ||
| const sx = health?.sessions; | ||
| if (sx && typeof sx.mode === 'string') { | ||
| const detail = sx.mode === 'pool' | ||
| ? `${sx.stickyBindings ?? 0} sticky binding${sx.stickyBindings === 1 ? '' : 's'} — conversation→account affinity, lazily reaped (6h idle TTL, cap 2000)` | ||
| : `${sx.active ?? 0} active session id${sx.active === 1 ? '' : 's'} — lazily reaped (LRU cap 1024, no background sweeper)`; | ||
| checks.push({ status: 'info', label: 'Sessions', detail }); | ||
| } | ||
| } | ||
| } | ||
| catch { /* proxy not running — live session state unavailable, skip */ } | ||
| // ---- Identity drift (pool account snapshot vs live ~/.claude.json) | ||
@@ -810,0 +830,0 @@ try { |
@@ -25,2 +25,14 @@ /** | ||
| version?: string; | ||
| /** | ||
| * Live session-tracking counts, surfaced to internal callers only. Both | ||
| * structures reap lazily (no background sweeper — see session-rotation.ts), | ||
| * so this raw in-memory size also reveals whether lazy cleanup keeps up. | ||
| */ | ||
| sessions?: { | ||
| mode: 'pool'; | ||
| stickyBindings: number; | ||
| } | { | ||
| mode: 'single'; | ||
| active: number; | ||
| }; | ||
| } | ||
@@ -27,0 +39,0 @@ export interface HealthResponse { |
@@ -84,2 +84,3 @@ /** | ||
| requests: requestCount, | ||
| ...(s.sessions ? { sessions: s.sessions } : {}), | ||
| ...(s.refreshFailures ? { refreshFailures: s.refreshFailures } : {}), | ||
@@ -86,0 +87,0 @@ } |
@@ -285,3 +285,3 @@ /** | ||
| readonly min: "1.0.0"; | ||
| readonly maxTested: "2.1.212"; | ||
| readonly maxTested: "2.1.214"; | ||
| }; | ||
@@ -288,0 +288,0 @@ /** |
@@ -809,3 +809,3 @@ /** | ||
| min: '1.0.0', | ||
| maxTested: '2.1.212', | ||
| maxTested: '2.1.214', | ||
| }; | ||
@@ -812,0 +812,0 @@ /** |
+1
-1
@@ -195,3 +195,3 @@ /** | ||
| */ | ||
| selectSticky(stickyKey: string | null, family?: string | null): PoolAccount | null; | ||
| selectSticky(stickyKey: string | null, family?: string | null, now?: number): PoolAccount | null; | ||
| /** | ||
@@ -198,0 +198,0 @@ * Rebind a sticky key to a different account — called by proxy after an |
+19
-11
@@ -160,3 +160,3 @@ /** | ||
| } | ||
| const STICKY_TTL_MS = 6 * 60 * 60 * 1000; // 6h | ||
| const STICKY_IDLE_TTL_MS = 6 * 60 * 60 * 1000; // reap a binding 6h after its LAST use, not its creation | ||
| const STICKY_MAX_ENTRIES = 2_000; // lazy cleanup cap | ||
@@ -338,10 +338,9 @@ const STICKY_CLEANUP_INTERVAL_MS = 30_000; // amortize the O(n) TTL/orphan sweep | ||
| */ | ||
| selectSticky(stickyKey, family) { | ||
| selectSticky(stickyKey, family, now = Date.now()) { | ||
| if (!stickyKey) | ||
| return this.select(family); | ||
| this.cleanupSticky(); | ||
| this.cleanupSticky(now); | ||
| const binding = this.sticky.get(stickyKey); | ||
| if (binding) { | ||
| const bound = this.accounts.get(binding.alias); | ||
| const now = Date.now(); | ||
| if (bound | ||
@@ -352,2 +351,6 @@ && bound.rateLimit.status !== 'rejected' | ||
| && computeHeadroom(bound.rateLimit, family) > POOL_HEADROOM_FLOOR) { | ||
| // Refresh the idle timer. A session that keeps taking turns must never | ||
| // be reaped or rebound while active — that would strand its warm prompt | ||
| // cache — so the TTL is re-based to now on every hit. | ||
| binding.lastUsedAt = now; | ||
| return bound; | ||
@@ -358,3 +361,3 @@ } | ||
| if (picked) { | ||
| this.sticky.set(stickyKey, { alias: picked.alias, boundAt: Date.now() }); | ||
| this.sticky.set(stickyKey, { alias: picked.alias, boundAt: now, lastUsedAt: now }); | ||
| } | ||
@@ -374,3 +377,4 @@ return picked; | ||
| return; | ||
| this.sticky.set(stickyKey, { alias, boundAt: Date.now() }); | ||
| const now = Date.now(); | ||
| this.sticky.set(stickyKey, { alias, boundAt: now, lastUsedAt: now }); | ||
| } | ||
@@ -383,4 +387,3 @@ /** | ||
| */ | ||
| cleanupSticky() { | ||
| const now = Date.now(); | ||
| cleanupSticky(now = Date.now()) { | ||
| // TTL/orphan sweep is O(n); amortize it — run at most once per | ||
@@ -393,3 +396,6 @@ // STICKY_CLEANUP_INTERVAL_MS instead of on every selectSticky (#642-audit). | ||
| for (const [key, b] of this.sticky) { | ||
| if (!this.accounts.has(b.alias) || now - b.boundAt > STICKY_TTL_MS) { | ||
| // Reap orphans (account gone) and bindings idle past the TTL. Idle is | ||
| // measured from lastUsedAt, which selectSticky refreshes every turn, so | ||
| // an actively-running conversation is never reaped here. | ||
| if (!this.accounts.has(b.alias) || now - b.lastUsedAt > STICKY_IDLE_TTL_MS) { | ||
| this.sticky.delete(key); | ||
@@ -401,6 +407,8 @@ } | ||
| // the O(n log n) sort amortizes over many inserts rather than firing on every | ||
| // new conversation at the cap (#642-audit). | ||
| // new conversation at the cap (#642-audit). Evict least-recently-USED first | ||
| // (true LRU): a binding's only value is its warm prompt cache, and the ones | ||
| // untouched longest are the coldest — least worth keeping. | ||
| if (this.sticky.size > STICKY_MAX_ENTRIES) { | ||
| const target = Math.floor(STICKY_MAX_ENTRIES * 0.8); | ||
| const sorted = [...this.sticky.entries()].sort((a, b) => a[1].boundAt - b[1].boundAt); | ||
| const sorted = [...this.sticky.entries()].sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt); | ||
| const toDrop = sorted.slice(0, this.sticky.size - target); | ||
@@ -407,0 +415,0 @@ for (const [key] of toDrop) |
+1
-1
| { | ||
| "name": "@askalf/dario", | ||
| "version": "5.2.7", | ||
| "version": "5.2.8", | ||
| "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,0 +5,0 @@ "type": "module", |
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1422676
0.21%27519
0.18%123
0.82%45
2.27%