@goodandready/dsh-key-rotation 0.8.88 → 0.8.89

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.89 (2026-11-03)
4
+
5
+ ### Interactive Header Chip UI & In-Chat Key Controls (Block 16 Release)
6
+ - **Interactive Header Chip Component (Issue #577)**: Enhanced `KeyRotationHeaderChip` (`lib/client-src/60-header-chip.js`) into a rich, interactive control center directly in the DeepSeek Harness chat header.
7
+ - **In-Chat Key Pinning & Selection**: Added instant dropdown selection to pin (`pinnedRef`) any key in the pool or restore `Auto (Round-robin)` rotation without opening the full settings page.
8
+ - **Prompt Cache & Health Badges**: Displays live prompt cache hit counters (`⚡ {promptCacheHits}`) and active/pinned key tails (`...tail`) with real-time health indicator status.
9
+ - **Quick-Action API Pinning Support**: Extended `/dsh-key-rotation/quick-action` route with `action: 'pin'` and audit event emission (`KEY_PINNED` / `KEY_UNPINNED`).
10
+ - **Detailed Health Endpoint**: Enriched `/dsh-key-rotation/health` to expose pool key manifests, individual key statuses (`active`, `pinned`, `paused`, `cooling`), and prompt cache hit statistics.
11
+
3
12
  ## 0.8.88 (2026-11-03)
4
13
 
5
14
  ### Subagent Concurrency Partitions & Task Isolation (Block 15 Release)
@@ -245,6 +245,11 @@
245
245
  quickResetTitle: 'Reset cooldowns and circuit breaker for active provider',
246
246
  quickPause: 'Pause',
247
247
  quickPauseTitle: 'Pause or resume active key',
248
+ quickPin: 'Pin',
249
+ quickPinTitle: 'Pin specific key or select Auto round-robin',
250
+ quickAuto: 'Auto (Round-robin)',
251
+ cacheHits: 'Cache: {n}',
252
+ pinnedBadge: 'pinned',
248
253
  exportConfig: 'Export Pools',
249
254
  importConfig: 'Import Pools',
250
255
  exportSafe: 'Masked (Safe for sharing)',
@@ -518,6 +523,11 @@
518
523
  quickResetTitle: '重置当前提供商的冷却时间与断路器',
519
524
  quickPause: '暂停',
520
525
  quickPauseTitle: '暂停或恢复当前正在使用的密钥',
526
+ quickPin: '固定',
527
+ quickPinTitle: '固定指定密钥或设为自动轮换',
528
+ quickAuto: '自动轮换',
529
+ cacheHits: '缓存: {n}',
530
+ pinnedBadge: '已固定',
521
531
  exportConfig: '导出密钥池',
522
532
  importConfig: '导入密钥池',
523
533
  exportSafe: '脱敏导出 (安全分享)',
@@ -1,5 +1,5 @@
1
- // (issue 201, #499) header chip: one dot + counts for all pools. Green = all healthy,
2
- // amber = some keys cooling, red = a pool fully exhausted. Click opens quick actions popover.
1
+ // (issue 201, #499, #577) header chip: one dot + counts for all pools. Green = all healthy,
2
+ // amber = some keys cooling, red = a pool fully exhausted. Click opens interactive quick actions popover.
3
3
  function KeyRotationHeaderChip(props) {
4
4
  const t = resolveT(props);
5
5
  const [snap, setSnap] = React.useState(null);
@@ -93,46 +93,101 @@
93
93
  <span style={{ fontSize: '9px', opacity: 0.6 }}>{open ? '▲' : '▼'}</span>
94
94
  </button>
95
95
  {open ? (
96
- <div className="krot-popover" style={{ minWidth: '220px' }}>
97
- <div className="krot-pop-title">{t('headerPoolsTitle')}</div>
96
+ <div className="krot-popover" style={{ minWidth: '260px' }}>
97
+ <div className="krot-pop-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
98
+ <span>{t('headerPoolsTitle')}</span>
99
+ {busy ? <span style={{ fontSize: '11px', opacity: 0.7 }}>...</span> : null}
100
+ </div>
98
101
  {!poolEntries.length ? (
99
102
  <div style={{ fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' }}>{t('noActivePools')}</div>
100
103
  ) : null}
101
104
  {poolEntries.map(([name, p]) => {
102
105
  const c = p.exhausted ? 'var(--dsw-alias-state-error-primary)' : (p.healthy < p.total ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-success-primary)');
103
106
  const provName = p.providerName || name;
104
- const tail = p.lastUsedTail;
107
+ const activeTail = p.lastUsedTail;
108
+ const pinnedTail = p.pinnedTail;
109
+ const isPinned = Boolean(p.pinnedRef);
110
+ const keysList = Array.isArray(p.keys) ? p.keys : [];
111
+ const cacheHits = p.promptCacheHits || 0;
112
+
105
113
  return (
106
- <div key={name} className="krot-pop-row" style={{ display: 'flex', flexDirection: 'column', gap: '4px', padding: '6px 0', borderBottom: '1px solid var(--dsw-alias-border-subtle)' }}>
114
+ <div key={name} className="krot-pop-row" style={{ display: 'flex', flexDirection: 'column', gap: '5px', padding: '8px 0', borderBottom: '1px solid var(--dsw-alias-border-subtle)' }}>
107
115
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
108
- <div className="krot-pop-name">
116
+ <div className="krot-pop-name" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
109
117
  <span style={{ width: '7px', height: '7px', borderRadius: '50%', background: c, flex: 'none' }} />
110
- {name}
111
- {tail ? <span style={{ marginLeft: '6px', fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)', fontFeatureSettings: '"tnum"', fontFamily: 'monospace' }}>{`...${tail}`}</span> : null}
118
+ <strong style={{ fontSize: '12px' }}>{name}</strong>
112
119
  </div>
113
- <div className="krot-pop-count" style={{ color: c, fontWeight: 600 }}>{`${p.healthy}/${p.total}`}</div>
120
+ <div className="krot-pop-count" style={{ color: c, fontWeight: 600, fontSize: '11px' }}>{`${p.healthy}/${p.total}`}</div>
121
+ </div>
122
+
123
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' }}>
124
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
125
+ <span>{isPinned ? '📌' : '⚡'}</span>
126
+ <span style={{ fontFamily: 'monospace' }}>
127
+ {isPinned ? (pinnedTail ? `...${pinnedTail} (pinned)` : p.pinnedRef) : (activeTail ? `...${activeTail}` : 'auto')}
128
+ </span>
129
+ </span>
130
+ {cacheHits > 0 ? (
131
+ <span style={{ fontSize: '10px', padding: '1px 4px', borderRadius: '3px', background: 'var(--dsw-alias-bg-subtle, transparent)', color: 'var(--dsw-alias-state-success-primary)' }} title="Prompt cache hits">
132
+ {`⚡ ${cacheHits}`}
133
+ </span>
134
+ ) : null}
114
135
  </div>
115
- <div style={{ display: 'flex', gap: '4px', marginTop: '2px' }}>
136
+
137
+ {keysList.length > 1 ? (
138
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '2px' }}>
139
+ <select
140
+ disabled={busy}
141
+ value={p.pinnedRef || ''}
142
+ onChange={(e) => runAction('pin', provName, e.target.value)}
143
+ title={t('quickPinTitle')}
144
+ style={{
145
+ width: '100%',
146
+ fontSize: '11px',
147
+ padding: '3px 6px',
148
+ borderRadius: '4px',
149
+ border: '1px solid var(--dsw-alias-border-subtle)',
150
+ background: 'var(--dsw-alias-bg-subtle, transparent)',
151
+ color: 'inherit',
152
+ fontFamily: 'inherit',
153
+ cursor: 'pointer'
154
+ }}
155
+ >
156
+ <option value="">{`🔀 ${t('quickAuto')}`}</option>
157
+ {keysList.map((k) => {
158
+ const marker = k.pinned ? '📌 ' : (k.active ? '▶ ' : '');
159
+ const status = k.paused ? ' [paused]' : (k.cooling ? ' [cooling]' : '');
160
+ return (
161
+ <option key={k.ref} value={k.ref}>
162
+ {`${marker}...${k.tail || k.ref.slice(-4)}${status}`}
163
+ </option>
164
+ );
165
+ })}
166
+ </select>
167
+ </div>
168
+ ) : null}
169
+
170
+ <div style={{ display: 'flex', gap: '4px', marginTop: '3px' }}>
116
171
  <button
117
172
  type="button"
118
173
  disabled={busy}
119
174
  onClick={(e) => { e.stopPropagation(); runAction('skip', provName, p.lastUsed); }}
120
175
  title={t('quickSkipTitle')}
121
- style={{ padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
176
+ style={{ flex: 1, padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
122
177
  >{`⏭️ ${t('quickSkip')}`}</button>
123
178
  <button
124
179
  type="button"
125
180
  disabled={busy}
126
181
  onClick={(e) => { e.stopPropagation(); runAction('reset', provName, p.lastUsed); }}
127
182
  title={t('quickResetTitle')}
128
- style={{ padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
183
+ style={{ flex: 1, padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
129
184
  >{`🔄 ${t('quickReset')}`}</button>
130
185
  <button
131
186
  type="button"
132
187
  disabled={busy || !p.lastUsed}
133
188
  onClick={(e) => { e.stopPropagation(); runAction('pause', provName, p.lastUsed); }}
134
189
  title={t('quickPauseTitle')}
135
- style={{ padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
190
+ style={{ flex: 1, padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
136
191
  >{`⏸️ ${t('quickPause')}`}</button>
137
192
  </div>
138
193
  </div>
package/lib/client.js CHANGED
@@ -264,6 +264,11 @@ window.__ModuleLoader__.load({
264
264
  quickResetTitle: "Reset cooldowns and circuit breaker for active provider",
265
265
  quickPause: "Pause",
266
266
  quickPauseTitle: "Pause or resume active key",
267
+ quickPin: "Pin",
268
+ quickPinTitle: "Pin specific key or select Auto round-robin",
269
+ quickAuto: "Auto (Round-robin)",
270
+ cacheHits: "Cache: {n}",
271
+ pinnedBadge: "pinned",
267
272
  exportConfig: "Export Pools",
268
273
  importConfig: "Import Pools",
269
274
  exportSafe: "Masked (Safe for sharing)",
@@ -536,6 +541,11 @@ window.__ModuleLoader__.load({
536
541
  quickResetTitle: "重置当前提供商的冷却时间与断路器",
537
542
  quickPause: "暂停",
538
543
  quickPauseTitle: "暂停或恢复当前正在使用的密钥",
544
+ quickPin: "固定",
545
+ quickPinTitle: "固定指定密钥或设为自动轮换",
546
+ quickAuto: "自动轮换",
547
+ cacheHits: "缓存: {n}",
548
+ pinnedBadge: "已固定",
539
549
  exportConfig: "导出密钥池",
540
550
  importConfig: "导入密钥池",
541
551
  exportSafe: "脱敏导出 (安全分享)",
@@ -3833,11 +3843,40 @@ window.__ModuleLoader__.load({
3833
3843
  /* @__PURE__ */ React.createElement("span", { style: { width: "8px", height: "8px", borderRadius: "50%", background: color, flex: "none", boxShadow: `0 0 6px ${color}` } }),
3834
3844
  label,
3835
3845
  /* @__PURE__ */ React.createElement("span", { style: { fontSize: "9px", opacity: 0.6 } }, open ? "▲" : "▼")
3836
- ), open ? /* @__PURE__ */ React.createElement("div", { className: "krot-popover", style: { minWidth: "220px" } }, /* @__PURE__ */ React.createElement("div", { className: "krot-pop-title" }, t("headerPoolsTitle")), !poolEntries.length ? /* @__PURE__ */ React.createElement("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-tertiary)" } }, t("noActivePools")) : null, poolEntries.map(([name, p]) => {
3846
+ ), open ? /* @__PURE__ */ React.createElement("div", { className: "krot-popover", style: { minWidth: "260px" } }, /* @__PURE__ */ React.createElement("div", { className: "krot-pop-title", style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, /* @__PURE__ */ React.createElement("span", null, t("headerPoolsTitle")), busy ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: "11px", opacity: 0.7 } }, "...") : null), !poolEntries.length ? /* @__PURE__ */ React.createElement("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-tertiary)" } }, t("noActivePools")) : null, poolEntries.map(([name, p]) => {
3837
3847
  const c = p.exhausted ? "var(--dsw-alias-state-error-primary)" : p.healthy < p.total ? "var(--dsw-alias-state-warn-primary)" : "var(--dsw-alias-state-success-primary)";
3838
3848
  const provName = p.providerName || name;
3839
- const tail = p.lastUsedTail;
3840
- return /* @__PURE__ */ React.createElement("div", { key: name, className: "krot-pop-row", style: { display: "flex", flexDirection: "column", gap: "4px", padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-subtle)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, /* @__PURE__ */ React.createElement("div", { className: "krot-pop-name" }, /* @__PURE__ */ React.createElement("span", { style: { width: "7px", height: "7px", borderRadius: "50%", background: c, flex: "none" } }), name, tail ? /* @__PURE__ */ React.createElement("span", { style: { marginLeft: "6px", fontSize: "11px", color: "var(--dsw-alias-label-tertiary)", fontFeatureSettings: '"tnum"', fontFamily: "monospace" } }, `...${tail}`) : null), /* @__PURE__ */ React.createElement("div", { className: "krot-pop-count", style: { color: c, fontWeight: 600 } }, `${p.healthy}/${p.total}`)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "4px", marginTop: "2px" } }, /* @__PURE__ */ React.createElement(
3849
+ const activeTail = p.lastUsedTail;
3850
+ const pinnedTail = p.pinnedTail;
3851
+ const isPinned = Boolean(p.pinnedRef);
3852
+ const keysList = Array.isArray(p.keys) ? p.keys : [];
3853
+ const cacheHits = p.promptCacheHits || 0;
3854
+ return /* @__PURE__ */ React.createElement("div", { key: name, className: "krot-pop-row", style: { display: "flex", flexDirection: "column", gap: "5px", padding: "8px 0", borderBottom: "1px solid var(--dsw-alias-border-subtle)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, /* @__PURE__ */ React.createElement("div", { className: "krot-pop-name", style: { display: "flex", alignItems: "center", gap: "6px" } }, /* @__PURE__ */ React.createElement("span", { style: { width: "7px", height: "7px", borderRadius: "50%", background: c, flex: "none" } }), /* @__PURE__ */ React.createElement("strong", { style: { fontSize: "12px" } }, name)), /* @__PURE__ */ React.createElement("div", { className: "krot-pop-count", style: { color: c, fontWeight: 600, fontSize: "11px" } }, `${p.healthy}/${p.total}`)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "11px", color: "var(--dsw-alias-label-tertiary)" } }, /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "4px" } }, /* @__PURE__ */ React.createElement("span", null, isPinned ? "📌" : "⚡"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "monospace" } }, isPinned ? pinnedTail ? `...${pinnedTail} (pinned)` : p.pinnedRef : activeTail ? `...${activeTail}` : "auto")), cacheHits > 0 ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: "10px", padding: "1px 4px", borderRadius: "3px", background: "var(--dsw-alias-bg-subtle, transparent)", color: "var(--dsw-alias-state-success-primary)" }, title: "Prompt cache hits" }, `⚡ ${cacheHits}`) : null), keysList.length > 1 ? /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: "6px", marginTop: "2px" } }, /* @__PURE__ */ React.createElement(
3855
+ "select",
3856
+ {
3857
+ disabled: busy,
3858
+ value: p.pinnedRef || "",
3859
+ onChange: (e) => runAction("pin", provName, e.target.value),
3860
+ title: t("quickPinTitle"),
3861
+ style: {
3862
+ width: "100%",
3863
+ fontSize: "11px",
3864
+ padding: "3px 6px",
3865
+ borderRadius: "4px",
3866
+ border: "1px solid var(--dsw-alias-border-subtle)",
3867
+ background: "var(--dsw-alias-bg-subtle, transparent)",
3868
+ color: "inherit",
3869
+ fontFamily: "inherit",
3870
+ cursor: "pointer"
3871
+ }
3872
+ },
3873
+ /* @__PURE__ */ React.createElement("option", { value: "" }, `🔀 ${t("quickAuto")}`),
3874
+ keysList.map((k) => {
3875
+ const marker = k.pinned ? "📌 " : k.active ? "▶ " : "";
3876
+ const status = k.paused ? " [paused]" : k.cooling ? " [cooling]" : "";
3877
+ return /* @__PURE__ */ React.createElement("option", { key: k.ref, value: k.ref }, `${marker}...${k.tail || k.ref.slice(-4)}${status}`);
3878
+ })
3879
+ )) : null, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "4px", marginTop: "3px" } }, /* @__PURE__ */ React.createElement(
3841
3880
  "button",
3842
3881
  {
3843
3882
  type: "button",
@@ -3847,7 +3886,7 @@ window.__ModuleLoader__.load({
3847
3886
  runAction("skip", provName, p.lastUsed);
3848
3887
  },
3849
3888
  title: t("quickSkipTitle"),
3850
- style: { padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
3889
+ style: { flex: 1, padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
3851
3890
  },
3852
3891
  `⏭️ ${t("quickSkip")}`
3853
3892
  ), /* @__PURE__ */ React.createElement(
@@ -3860,7 +3899,7 @@ window.__ModuleLoader__.load({
3860
3899
  runAction("reset", provName, p.lastUsed);
3861
3900
  },
3862
3901
  title: t("quickResetTitle"),
3863
- style: { padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
3902
+ style: { flex: 1, padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
3864
3903
  },
3865
3904
  `🔄 ${t("quickReset")}`
3866
3905
  ), /* @__PURE__ */ React.createElement(
@@ -3873,7 +3912,7 @@ window.__ModuleLoader__.load({
3873
3912
  runAction("pause", provName, p.lastUsed);
3874
3913
  },
3875
3914
  title: t("quickPauseTitle"),
3876
- style: { padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
3915
+ style: { flex: 1, padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
3877
3916
  },
3878
3917
  `⏸️ ${t("quickPause")}`
3879
3918
  )));
@@ -35,8 +35,8 @@ export function registerQuickActionRoutes(ctx, deps) {
35
35
  const provider = typeof body?.provider === 'string' ? body.provider.trim() : '';
36
36
  const ref = typeof body?.ref === 'string' ? body.ref.trim() : null;
37
37
 
38
- if (!action || !['skip', 'reset', 'pause'].includes(action)) {
39
- json(res, 400, { error: { code: 'invalid-action', message: 'action must be skip, reset, or pause' } });
38
+ if (!action || !['skip', 'reset', 'pause', 'pin'].includes(action)) {
39
+ json(res, 400, { error: { code: 'invalid-action', message: 'action must be skip, reset, pause, or pin' } });
40
40
  return;
41
41
  }
42
42
 
@@ -164,6 +164,30 @@ export function registerQuickActionRoutes(ctx, deps) {
164
164
  });
165
165
  return;
166
166
  }
167
+ if (action === 'pin') {
168
+ const targetRef = typeof ref === 'string' && ref.trim().length > 0 ? ref.trim() : null;
169
+ if (targetRef && targetPool.refs && !targetPool.refs.includes(targetRef)) {
170
+ json(res, 400, { error: { code: 'invalid-key', message: `Key ${targetRef} not in pool` } });
171
+ return;
172
+ }
173
+ targetPool.pinnedRef = targetRef;
174
+ if (targetRef) {
175
+ targetPool.state.lastUsed = targetRef;
176
+ }
177
+
178
+ if (typeof pushEvent === 'function') {
179
+ pushEvent(targetPool, targetRef || '*', targetRef ? 'KEY_PINNED' : 'KEY_UNPINNED', 0);
180
+ }
181
+
182
+ json(res, 200, {
183
+ ok: true,
184
+ action: 'pin',
185
+ provider,
186
+ pinnedRef: targetPool.pinnedRef,
187
+ pinnedTail: targetPool.pinnedRef ? keyTail(targetPool.pinnedRef) : null,
188
+ });
189
+ return;
190
+ }
167
191
  },
168
192
  }));
169
193
  }
@@ -249,6 +249,17 @@ ctx.effect(() => getWebServer(ctx).register({
249
249
  modelQuota: quotaAware ? { configured: true, exhaustedKeys: quotaExhausted } : null,
250
250
  lastUsed: pool.state.lastUsed ?? null,
251
251
  lastUsedTail: pool.state.lastUsed ? keyTail(pool.state.lastUsed) : null,
252
+ pinnedRef: pool.pinnedRef ?? null,
253
+ pinnedTail: pool.pinnedRef ? keyTail(pool.pinnedRef) : null,
254
+ promptCacheHits: pool.state.promptCacheHits ?? 0,
255
+ keys: pool.refs.map((r) => ({
256
+ ref: r,
257
+ tail: keyTail(r),
258
+ active: pool.state.lastUsed === r,
259
+ pinned: pool.pinnedRef === r,
260
+ paused: isKeyPaused(pool, r),
261
+ cooling: (pool.state.failedUntil?.get(r) ?? 0) > now,
262
+ })),
252
263
  };
253
264
  }
254
265
  let circuitOpenAny = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.88",
3
+ "version": "0.8.89",
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": [