@goodandready/dsh-key-rotation 0.7.38 → 0.7.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/README.md CHANGED
@@ -253,6 +253,11 @@ All management routes require loopback authentication (`127.0.0.1` / `::1`) with
253
253
 
254
254
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
255
255
 
256
+ ### v0.7.39
257
+ - **Self-Healing & lastUsedAt Accuracy**: Fixed key timestamp lookup in `healIdleCooldowns` to read from the modern `pool.state.lastUsedAt` map (with backwards-compatible fallback). `credentials.resolve` now properly records each key invocation timestamp in `lastUsedAt`, surfacing accurate "last used" indicators in the status dashboard and enabling background idle cooldown restoration.
258
+ - **Hotpath & Runtime Memoization**: Eliminated redundant `buildRuntime()` calls across periodic sweeps, provider exhaustion handling, and `/status` query processing.
259
+ - **Test Suite & CI Hardening**: Periodic interval sweep timers and debounce timers unref'ed to allow Node.js event loop natural exit without stalling CI runners. Purged obsolete incident test artifacts.
260
+
256
261
  ### v0.7.38
257
262
  - **Hot-Path Stream Optimization**: Eliminated 4 redundant `buildRuntime()` calls inside the `rotate()` finish chunk handler by reusing the request-scoped `runtime0` snapshot.
258
263
  - **Allocation-Free Rate Limit Header Parsing**: Optimized `extractRateLimit()` with single-pass header inspection and length guards, completely removing dynamic lowercase/uppercase string allocations on every response chunk.
package/README.ru.md CHANGED
@@ -253,6 +253,11 @@ dsh-key-rotation:
253
253
 
254
254
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
255
255
 
256
+ ### v0.7.39
257
+ - **Самоисцеление ключей и фиксация lastUsedAt**: Исправлено чтение меток времени в `healIdleCooldowns`, использующее теперь карту `pool.state.lastUsedAt`. В `credentials.resolve` добавлено сохранение точного времени каждого обращения к ключу, что активирует вывод времени использования в панели мониторинга и корректное самоисцеление ключей после простоя.
258
+ - **Оптимизация обращений к buildRuntime**: Устранены повторные вызовы `buildRuntime()` в процедурах периодической очистки, маршруте `/status` и блоке обработки исчерпания пула.
259
+ - **Стабилизация CI и устранение утечек таймеров**: Фоновые таймеры дебаунса и очистки теперь отвязываются (`unref`) от цикла событий Node.js, предотвращая зависание раннера. Удалены тесты устаревшего модуля инцидентов.
260
+
256
261
  ### v0.7.38
257
262
  - **Оптимизация горячего пути стриминга**: Устранены 4 избыточных вызова `buildRuntime()` при обработке завершающего чанка в `rotate()` за счёт повторного использования снимка `runtime0`.
258
263
  - **Парсинг заголовков лимитов без лишних аллокаций**: Оптимизирована функция `extractRateLimit()` — однократный проход по объекту с проверкой длины ключей без постоянных аллокаций строк `.toLowerCase()` и `.toUpperCase()`.
package/README.zh.md CHANGED
@@ -214,6 +214,11 @@ dsh-key-rotation:
214
214
 
215
215
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
216
216
 
217
+ ### v0.7.39
218
+ - **自动恢复与 lastUsedAt 修复**:修复了 `healIdleCooldowns` 中对密钥调用时间戳的读取逻辑,直接从 `lastUsedAt` 映射读取。在 `credentials.resolve` 中补充记录每次密钥调用的时间戳,使状态面板的最近使用时间生效并正确支持空闲密钥恢复。
219
+ - **运行期快照缓存优化**:消除了定时维护清理、`/status` 路由及密钥池耗尽处理中重复调用 `buildRuntime()` 的开销。
220
+ - **CI 测试稳定性提升**:对维护定时器与通知防抖定时器执行 `unref`,确保 Node.js 事件循环在测试完成后干净退出,彻底解决 CI 运行器假死问题。
221
+
217
222
  ### v0.7.38
218
223
  - **热路径流处理优化**:在 `rotate()` 结束块处理中消除 4 次多余的 `buildRuntime()` 重复调用,直接复用请求作用域内的 `runtime0` 快照。
219
224
  - **零额外字符串分配的限流头解析**:重构 `extractRateLimit()`,采用单次遍历结合键长度检查,彻底消除对每个响应头执行 `.toLowerCase()` / `.toUpperCase()` 的内存碎片分配。
package/lib/heal.js CHANGED
@@ -11,13 +11,13 @@ export function healIdleCooldowns(pools, idleMs, now = Date.now()) {
11
11
  for (const pool of pools) {
12
12
  if (!pool || !pool.state || !pool.base) continue;
13
13
  const fu = pool.state.failedUntil;
14
- const lu = pool.state.lastUsed;
14
+ const lua = pool.state.lastUsedAt ?? pool.state.lastUsed;
15
15
  if (!fu || fu.size === 0) continue;
16
16
  const expiredRefs = [];
17
17
  for (const [ref, until] of fu.entries()) {
18
18
  if (!Number.isFinite(until)) continue;
19
19
  if (until > now) continue; // cooldown still active
20
- const last = lu ? lu.get(ref) : undefined;
20
+ const last = typeof lua?.get === 'function' ? lua.get(ref) : undefined;
21
21
  if (!Number.isFinite(last)) continue; // never used → no signal, skip
22
22
  if (now - last < idleMs) continue; // used recently → don't heal
23
23
  expiredRefs.push(ref);
package/lib/index.js CHANGED
@@ -38,7 +38,7 @@ import { keyTail, isLoopbackAddress, isTrustedBridgeRequest, SWITCHABLE_MESSAGE_
38
38
 
39
39
  export const name = 'dsh-key-rotation';
40
40
  export const inject = ['llm', 'webServer', 'settings', 'credentials'];
41
- export { keyTail, isLoopbackAddress, isTrustedBridgeRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage };
41
+ export { keyTail, isLoopbackAddress, isTrustedBridgeRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage, getRuntime };
42
42
 
43
43
  /** Settings namespace owning the GUI-editable section (settingsNamespace-valid). */
44
44
  const NS = 'dsh-key-rotation';
@@ -452,13 +452,13 @@ export function apply(ctx, config = {}) {
452
452
  }
453
453
  const n = sweepExpired(poolState, now);
454
454
  if (n > 0) console.warn(`[dsh-key-rotation] sweep: cleared ${n} expired cooldown(s)`);
455
- for (const pool of buildRuntime().poolByRef.values()) {
455
+ const runtime = buildRuntime();
456
+ for (const pool of runtime.poolByRef.values()) {
456
457
  compactUsage(pool, 30, now);
457
458
  }
458
459
  // #207 expiry pre-warning + #208 cost budget - piggybacked on this timer,
459
460
  // deduped to one notification per key/window per day (shouldNotifyDaily).
460
461
  try {
461
- const runtime = buildRuntime();
462
462
  const seen = new Set();
463
463
  for (const pool of runtime.poolByRef.values()) {
464
464
  if (seen.has(pool.base)) continue;
@@ -563,6 +563,7 @@ export function apply(ctx, config = {}) {
563
563
  }
564
564
  } catch (_) { /* maintenance must never crash the sweep */ }
565
565
  }, 30000);
566
+ if (typeof id.unref === 'function') id.unref();
566
567
  return () => clearInterval(id);
567
568
  }, 'dsh-key-rotation: sweep expired cooldowns');
568
569
 
@@ -771,6 +772,8 @@ export function apply(ctx, config = {}) {
771
772
  let hit = await original(candidate);
772
773
  if (hit && typeof hit.value === 'string' && hit.value.length > 0) {
773
774
  pool.state.lastUsed = candidate;
775
+ if (!pool.state.lastUsedAt) pool.state.lastUsedAt = new Map();
776
+ pool.state.lastUsedAt.set(candidate, now);
774
777
  const store = dispatchStorage.getStore();
775
778
  if (store && store.pool === pool) store.pickedRef = candidate;
776
779
  if (pool.state.failCounts) pool.state.failCounts.delete(candidate);
@@ -792,6 +795,8 @@ export function apply(ctx, config = {}) {
792
795
  const envVal = envValue(candidate);
793
796
  if (envVal !== undefined) {
794
797
  pool.state.lastUsed = candidate;
798
+ if (!pool.state.lastUsedAt) pool.state.lastUsedAt = new Map();
799
+ pool.state.lastUsedAt.set(candidate, now);
795
800
  const store = dispatchStorage.getStore();
796
801
  if (store && store.pool === pool) store.pickedRef = candidate;
797
802
  if (pool.state.failCounts) pool.state.failCounts.delete(candidate);
@@ -1034,11 +1039,11 @@ export function apply(ctx, config = {}) {
1034
1039
  pool.state.lastExhaustionAt = Date.now();
1035
1040
  pool.state.exhaustionCount = (pool.state.exhaustionCount ?? 0) + 1;
1036
1041
  console.warn(`[dsh-key-rotation] ${options.provider}: pool exhausted — all ${pool.refs.length} keys cooling`);
1042
+ const runtime = buildRuntime();
1037
1043
  // notify via extracted helper (see notifyExhaustion above)
1038
- notifyExhaustion(buildRuntime(), pool, { provider: options.provider });
1044
+ notifyExhaustion(runtime, pool, { provider: options.provider });
1039
1045
 
1040
1046
  // #194: cross-provider cascade failover (guarded against infinite recursion)
1041
- const runtime = buildRuntime();
1042
1047
  if (!options.__isCascade && Array.isArray(runtime.cascade) && runtime.cascade.length > 0) {
1043
1048
  const pools = runtime.providerToPool;
1044
1049
  const fb = pickCascadeFallback(options.provider, runtime, pools);
@@ -1079,10 +1084,10 @@ export function apply(ctx, config = {}) {
1079
1084
  json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: status is local-only' } });
1080
1085
  return;
1081
1086
  }
1082
- const { poolByRef, providerTags, providerBudgets } = buildRuntime();
1087
+ const runtime = buildRuntime();
1088
+ const { poolByRef, providerTags, providerBudgets, latencySloMs } = runtime;
1083
1089
  const base = ctx.get('credentials');
1084
1090
  const now = Date.now();
1085
- const latencySloMs = buildRuntime().latencySloMs;
1086
1091
  const seen = new Set();
1087
1092
  const providers = [];
1088
1093
  for (const pool of poolByRef.values()) {
package/lib/webhook.js CHANGED
@@ -157,6 +157,7 @@ export class AlertDebouncer {
157
157
  }
158
158
  if (!entry.timer) {
159
159
  entry.timer = setTimeout(() => this.flush(url), this._debounceMs);
160
+ if (typeof entry.timer.unref === 'function') entry.timer.unref();
160
161
  }
161
162
  }
162
163
 
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.7.38",
4
- "packageManager": "pnpm@10.33.2",
3
+ "version": "0.7.39",
5
4
  "description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
6
5
  "keywords": [
7
6
  "deepseek-harness",
@@ -54,6 +53,6 @@
54
53
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6"
55
54
  },
56
55
  "scripts": {
57
- "test": "node --test test/*.test.js test/*.test.mjs"
56
+ "test": "node --test --test-timeout=10000 test/*.test.js test/*.test.mjs"
58
57
  }
59
- }
58
+ }