@goodandready/dsh-key-rotation 0.8.57 → 0.8.58

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.58 - 2026-10-13
4
+
5
+ ### Fixed
6
+ - **Early-Warning Notification Dispatch (#477)**:
7
+ - Connected `notifyEarlyWarning` during key penalization in `lib/rotate.js` when healthy pool capacity drops to or below `earlyWarningKeyThreshold`.
8
+ - Added export `notifyEarlyWarning` in `lib/index.js` and wired dependency through `createRotate`.
9
+ - **Proactive Health Probing Logging (#478)**:
10
+ - Replaced empty catch block in `lib/lifecycle.js` `setupProactiveHealthProbeEffect` with structured debug logging (`logger?.debug`), eliminating silent swallowed exceptions.
11
+ - **Key Drain Lifecycle Teardown (#479)**:
12
+ - Registered `setupKeyDrainEffect` in `lib/lifecycle.js` to ensure module-level `drainingKeys` map is wiped cleanly on plugin teardown/dispose via `clearAllDraining()`.
13
+ - **Quota Reset Window Schema Validation (#480)**:
14
+ - Connected `QUOTA_WINDOW_TYPES` into `SettingsSchema` via `Schema.union(QUOTA_WINDOW_TYPES).default('midnight_utc')`.
15
+ - Connected `isBlockedUntilReset` in `lib/ops-status.js` to accurately expose calendar-based reset lock status.
16
+
3
17
  ## 0.8.57 - 2026-10-12
4
18
 
5
19
  ### Added
package/lib/index.js CHANGED
@@ -31,7 +31,8 @@ import { NotifyQueue } from './notify-queue.js';
31
31
  import { classifyFailure } from './error-taxonomy.js';
32
32
  import { registerOpsRoutes } from './routes-ops.js';
33
33
  import { registerPluginUpdater } from './plugin-updater.js';
34
- import { notifySwitch, notifyExhaustion, pushEvent } from './notify-events.js';
34
+ import { notifySwitch, notifyExhaustion, notifyEarlyWarning, pushEvent } from './notify-events.js';
35
+ import { QUOTA_WINDOW_TYPES } from './quota-window.js';
35
36
  import { getLogger } from './logger.js';
36
37
  import { createSandboxService } from './sandbox-service.js';
37
38
  import { checkBudgetAndHealthAlerts } from './budget-monitor.js';
@@ -44,12 +45,12 @@ import {
44
45
  formatModelQuotaExhaustion,
45
46
  } from './model-quota.js';
46
47
  import { LOCAL_QUOTA_CODE } from './rotate.js';
47
- import { setupIdleHealEffect, setupAutoUnbreakEffect, setupPersistence, setupProactiveHealthProbeEffect } from './lifecycle.js';
48
+ import { setupIdleHealEffect, setupAutoUnbreakEffect, setupPersistence, setupProactiveHealthProbeEffect, setupKeyDrainEffect } from './lifecycle.js';
48
49
 
49
50
  export const name = '@goodandready/dsh-key-rotation';
50
51
  export const inject = ['llm', 'webServer', 'settings', 'credentials'];
51
52
  export { keyTail, KEY_TAIL_CHARS, KEY_TAIL_PLACEHOLDER, isLoopbackAddress, isTrustedBridgeRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage, getRuntime };
52
- export { notifySwitch, notifyExhaustion };
53
+ export { notifySwitch, notifyExhaustion, notifyEarlyWarning };
53
54
 
54
55
  const NS = 'dsh-key-rotation';
55
56
 
@@ -88,7 +89,7 @@ const SettingsSchema = Schema.object({
88
89
  model: Schema.string(),
89
90
  })).default([]),
90
91
  quotaResetWindow: Schema.object({
91
- type: Schema.string().default('midnight_utc'),
92
+ type: Schema.union(QUOTA_WINDOW_TYPES).default('midnight_utc'),
92
93
  hour: Schema.number().default(0),
93
94
  }),
94
95
  rateLimitThreshold: Schema.number().default(0.1),
@@ -206,6 +207,7 @@ export function apply(ctx, config = {}) {
206
207
 
207
208
  setupIdleHealEffect(ctx, getConfig, buildRuntime);
208
209
  setupProactiveHealthProbeEffect(ctx, getConfig, buildRuntime, ensureSandboxRunner, logger);
210
+ setupKeyDrainEffect(ctx);
209
211
 
210
212
  const poolState = new Map();
211
213
  const { schedulePersist, persistenceSnapshot } = setupPersistence(ctx, {
@@ -421,6 +423,7 @@ export function apply(ctx, config = {}) {
421
423
  pushEvent,
422
424
  notifySwitch: (runtime, pool, info) => notifySwitch(runtime, pool, info, { webhookSender, notifyQueue: moduleNotifyQueue, now: () => Date.now() }),
423
425
  notifyExhaustion: (runtime, pool, options) => notifyExhaustion(runtime, pool, options, { webhookSender, notifyQueue: moduleNotifyQueue, now: () => Date.now() }),
426
+ notifyEarlyWarning: (runtime, pool, info) => notifyEarlyWarning(runtime, pool, info, { webhookSender, notifyQueue: moduleNotifyQueue, now: () => Date.now() }),
424
427
  recordLatency,
425
428
  latencyHistogram,
426
429
  concurrencyTracker,
package/lib/lifecycle.js CHANGED
@@ -4,6 +4,7 @@ import { decryptSecret } from './crypto-storage.js';
4
4
  import { StatePersistence, resolveStatePath } from './persistence.js';
5
5
  import path from 'node:path';
6
6
  import { envValue } from './pool.js';
7
+ import { clearAllDraining } from './key-drain.js';
7
8
 
8
9
  export function setupIdleHealEffect(ctx, getConfig, buildRuntime, timers = globalThis) {
9
10
  return ctx.effect(() => {
@@ -209,7 +210,9 @@ export function setupProactiveHealthProbeEffect(ctx, getConfig, buildRuntime, en
209
210
  }
210
211
  logger?.warn?.(`[dsh-key-rotation] proactive-probe: restored key ${ref} in pool ${pool.base}`);
211
212
  }
212
- } catch (_) {}
213
+ } catch (err) {
214
+ logger?.debug?.(`[dsh-key-rotation] proactive-probe failed for ${ref}: ${err?.message || err}`);
215
+ }
213
216
  }
214
217
  }
215
218
  } catch (e) {
@@ -244,3 +247,11 @@ export function setupProactiveHealthProbeEffect(ctx, getConfig, buildRuntime, en
244
247
  };
245
248
  }, 'dsh-key-rotation: proactive health probing');
246
249
  }
250
+
251
+ export function setupKeyDrainEffect(ctx) {
252
+ return ctx.effect(() => {
253
+ return () => {
254
+ clearAllDraining();
255
+ };
256
+ }, 'dsh-key-rotation: clear draining keys on teardown');
257
+ }
package/lib/ops-status.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  isLoopbackAddress,
16
16
  } from './pool.js';
17
17
  import { bucketInfo, tpmInfo } from './bucket.js';
18
+ import { isBlockedUntilReset } from './quota-window.js';
18
19
  import { sanitizeSnapshot } from './sanitize-snapshot.js';
19
20
  import {
20
21
  STATUS_PATH,
@@ -92,6 +93,7 @@ ctx.effect(() => ctx.webServer.register({
92
93
  /* describe is optional — the card falls back to editable */
93
94
  }
94
95
  const until = pool.state.failedUntil?.get(ref);
96
+ const quotaWin = pool.state.quotaWindows?.get(ref);
95
97
  const modelQuota = getModelQuotaStatus(pool, ref, now);
96
98
  const activeModelPools = runtime?.index ? modelPoolsForRef(runtime.index, ref) : [];
97
99
  keys.push({
@@ -102,6 +104,7 @@ ctx.effect(() => ctx.webServer.register({
102
104
  writable,
103
105
  active: pool.state.lastUsed === ref,
104
106
  cooldownMsLeft: until !== undefined && until > now ? until - now : 0,
107
+ blockedUntilReset: isBlockedUntilReset(until, quotaWin, now),
105
108
  // #210: RPM capacity snapshot (null when rpmLimit is off)
106
109
  rpm: bucketInfo(pool.state.rpmWindows, ref, pool.rpmLimit, now),
107
110
  // TPM capacity snapshot (null when tpmLimit is off)
package/lib/rotate.js CHANGED
@@ -19,6 +19,7 @@ import { pickCascadeFallback, CASCADE_MAX_DEPTH } from './cascade.js';
19
19
  import { createRequestBudget, BUDGET_EXHAUSTED_TIMEOUT, BUDGET_EXHAUSTED_ATTEMPTS } from './request-budget.js';
20
20
  import { createSelectionTrace } from './selection-trace.js';
21
21
  import { isKeyDraining } from './key-drain.js';
22
+ import { notifyEarlyWarning as defaultNotifyEarlyWarning } from './notify-events.js';
22
23
  import { nextQuotaReset, poolResetAt } from './quota-window.js';
23
24
  import { tpmRecord } from './bucket.js';
24
25
  import {
@@ -64,6 +65,7 @@ export function createRotate(deps) {
64
65
  pushEvent,
65
66
  notifySwitch,
66
67
  notifyExhaustion,
68
+ notifyEarlyWarning,
67
69
  recordLatency,
68
70
  latencyHistogram,
69
71
  concurrencyTracker,
@@ -77,6 +79,7 @@ export function createRotate(deps) {
77
79
  now = nowMono,
78
80
  } = deps;
79
81
 
82
+ const effectiveNotifyEarlyWarning = notifyEarlyWarning ?? defaultNotifyEarlyWarning;
80
83
  const logWarn = (msg) => (logger?.warn ? logger.warn(msg) : null);
81
84
 
82
85
  function rotate(options, pool) {
@@ -204,6 +207,24 @@ export function createRotate(deps) {
204
207
  if (typeof schedulePersist === 'function') {
205
208
  try { schedulePersist(); } catch (_) { /* best-effort persistence */ }
206
209
  }
210
+ if (runtime0.earlyWarningNotify && effectiveNotifyEarlyWarning) {
211
+ const total = pool.refs?.length || 1;
212
+ let available = 0;
213
+ const at = now();
214
+ for (const r of (pool.refs || [])) {
215
+ if (!isKeyRevoked(pool, r) && !isKeyPaused(pool, r) && !isKeyBroken(pool, r, at) && ((pool.state?.failedUntil?.get(r) ?? 0) <= at)) {
216
+ available++;
217
+ }
218
+ }
219
+ const threshold = Number.isFinite(runtime0.earlyWarningKeyThreshold) ? runtime0.earlyWarningKeyThreshold : 0.25;
220
+ if (total > 0 && (available / total) <= threshold) {
221
+ effectiveNotifyEarlyWarning(runtime0, pool, {
222
+ provider: options.provider,
223
+ availableCount: available,
224
+ totalCount: total,
225
+ });
226
+ }
227
+ }
207
228
  if (!pool.state.authFailCounts) pool.state.authFailCounts = new Map();
208
229
  if (!pool.state.brokenUntil) pool.state.brokenUntil = new Map();
209
230
  const _cStr = String(errCode ?? '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.57",
3
+ "version": "0.8.58",
4
4
  "packageManager": "pnpm@10.33.2",
5
5
  "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
6
  "keywords": [