@goodandready/dsh-key-rotation 0.8.69 → 0.8.70
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 +8 -0
- package/lib/clone-lifecycle.js +20 -3
- package/lib/index.js +1 -0
- package/lib/pool-builder.js +1 -2
- package/lib/pool.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.70 - 2026-10-20
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
- **Fix Fatal Crash in sweepExpired & Isolate Clone Metadata (#513, GitHub #26)**:
|
|
7
|
+
- Guarded `sweepExpired` in `lib/pool.js` and the periodic sweep timer in `lib/index.js` with `if (!st || !(st.failedUntil instanceof Map)) continue;` to safely skip non-pool-state values.
|
|
8
|
+
- Isolated clone route metadata tracking inside `CloneLifecycleManager` (`this.knownClones`) rather than polluting `poolState` with `Set` objects (`${provider}:clones`).
|
|
9
|
+
- Added regression test `test/github-issue-26-sweep-expired.test.mjs` verifying `sweepExpired` robustness with heterogeneous data and clean `poolState` boundaries.
|
|
10
|
+
|
|
3
11
|
## 0.8.69 - 2026-10-19
|
|
4
12
|
|
|
5
13
|
### Features
|
package/lib/clone-lifecycle.js
CHANGED
|
@@ -101,20 +101,37 @@ export class CloneLifecycleManager {
|
|
|
101
101
|
this.concurrencyTracker = concurrencyTracker;
|
|
102
102
|
this.logger = logger || console;
|
|
103
103
|
this.drainingClones = new Map(); // cloneId -> { provider, ref, startedAt, timer }
|
|
104
|
+
this.knownClones = new Map(); // provider -> Set<string>
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
108
|
* Evaluates previous vs current expected clones for a provider.
|
|
108
109
|
* If a clone was removed, marks it draining instead of instantly destroying it.
|
|
109
110
|
* @param {string} provider
|
|
110
|
-
* @param {Set<string>|string[]} prevClones
|
|
111
|
+
* @param {Set<string>|string[]} [prevClones]
|
|
111
112
|
* @param {Set<string>|string[]} currentClones
|
|
112
|
-
* @param {Function} onDeregister callback when draining completes and route is safe to remove
|
|
113
|
+
* @param {Function} [onDeregister] callback when draining completes and route is safe to remove
|
|
113
114
|
*/
|
|
114
|
-
reconcileClones(provider,
|
|
115
|
+
reconcileClones(provider, prevOrCurrent, currentOrDeregister, maybeDeregister) {
|
|
116
|
+
let prevClones;
|
|
117
|
+
let currentClones;
|
|
118
|
+
let onDeregister;
|
|
119
|
+
|
|
120
|
+
if (currentOrDeregister instanceof Set || Array.isArray(currentOrDeregister)) {
|
|
121
|
+
prevClones = prevOrCurrent;
|
|
122
|
+
currentClones = currentOrDeregister;
|
|
123
|
+
onDeregister = maybeDeregister;
|
|
124
|
+
} else {
|
|
125
|
+
prevClones = this.knownClones.get(provider) ?? [];
|
|
126
|
+
currentClones = prevOrCurrent;
|
|
127
|
+
onDeregister = currentOrDeregister;
|
|
128
|
+
}
|
|
129
|
+
|
|
115
130
|
const prevSet = prevClones instanceof Set ? prevClones : new Set(prevClones || []);
|
|
116
131
|
const currSet = currentClones instanceof Set ? currentClones : new Set(currentClones || []);
|
|
117
132
|
|
|
133
|
+
this.knownClones.set(provider, currSet);
|
|
134
|
+
|
|
118
135
|
for (const cloneId of prevSet) {
|
|
119
136
|
if (!currSet.has(cloneId)) {
|
|
120
137
|
this.startDrain(cloneId, provider, onDeregister);
|
package/lib/index.js
CHANGED
|
@@ -232,6 +232,7 @@ export function apply(ctx, config = {}) {
|
|
|
232
232
|
const id = setInterval(() => {
|
|
233
233
|
const now = Date.now();
|
|
234
234
|
for (const st of poolState.values()) {
|
|
235
|
+
if (!st || !(st.failedUntil instanceof Map)) continue;
|
|
235
236
|
for (const [ref, until] of [...(st.failedUntil?.entries() ?? [])]) {
|
|
236
237
|
if (until <= now && !st.probedAt?.has(ref)) {
|
|
237
238
|
st.events.push({ at: until, ref, reason: 'probe', cooldownMs: 0, type: 'probe' });
|
package/lib/pool-builder.js
CHANGED
|
@@ -285,8 +285,7 @@ export function cleanupRemovedProviders({
|
|
|
285
285
|
if (cloneLifecycle && typeof cloneLifecycle.reconcileClones === 'function') {
|
|
286
286
|
for (const p of cfg.providers ?? []) {
|
|
287
287
|
const liveClones = new Set([...expectedClones].filter(c => c.startsWith(`${p.provider}-`)));
|
|
288
|
-
cloneLifecycle.reconcileClones(p.provider,
|
|
289
|
-
poolState.set(`${p.provider}:clones`, liveClones);
|
|
288
|
+
cloneLifecycle.reconcileClones(p.provider, liveClones);
|
|
290
289
|
}
|
|
291
290
|
}
|
|
292
291
|
}
|
package/lib/pool.js
CHANGED
|
@@ -226,6 +226,7 @@ export function sweepExpired(poolState, now = Date.now(), activeRefs = null, quo
|
|
|
226
226
|
let cleared = 0;
|
|
227
227
|
const activeSet = activeRefs ? new Set(activeRefs) : null;
|
|
228
228
|
for (const st of poolState.values()) {
|
|
229
|
+
if (!st || !(st.failedUntil instanceof Map)) continue;
|
|
229
230
|
// #338: decay penalty failCounts for stable keys in maintenance sweep
|
|
230
231
|
decayPenalties({ state: st }, now);
|
|
231
232
|
for (const [ref, until] of [...st.failedUntil.entries()]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.70",
|
|
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": [
|