@goodandready/dsh-usage-guard 0.1.5 → 0.1.6
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 +14 -0
- package/README.ru.md +14 -0
- package/lib/client.js +489 -185
- package/lib/index.js +14 -7
- package/lib/usage.js +25 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,20 @@ Logs informative diagnostic warnings naming the exact session, turn, step, raw p
|
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
|
|
115
|
+
## 🚀 Changed in v0.1.6
|
|
116
|
+
|
|
117
|
+
* **Design Alignment with `dsh-clinebot` (#6)**:
|
|
118
|
+
- Idempotent `ensureCss()` outside component render with `<style id="dsh-usage-guard-full-css" data-dsh-plugin="dsh-usage-guard">`.
|
|
119
|
+
- Wrapped settings card in an `ErrorBoundary` to gracefully contain render errors and offer a "Retry" mechanism without breaking DSH Settings.
|
|
120
|
+
- Native DSH theme tokens for card styling (`.ug-section-card`, `.ug-field-card`, `.ug-stat-box`, `.ug-badge-ok`, `.ug-badge-warn`, `.ug-btn-primary`).
|
|
121
|
+
- Added protection status telemetry grid displaying current operational mode and target Cordis service.
|
|
122
|
+
- Built-in `makeT(dict, fallback)` supporting Russian and English locales with template interpolation.
|
|
123
|
+
- Added `refreshMirrorUntilVisible(ctx)` with an unref timer to guarantee host settings scope visibility.
|
|
124
|
+
* **Parser Stability & Cache Aliases Hardening**:
|
|
125
|
+
- Added recognition of nested OpenAI cache details (`prompt_tokens_details.cachedTokens`, `prompt_tokens_details.cacheCreationTokens`).
|
|
126
|
+
- Hardened number coercion and sanitization against `Infinity`, `-Infinity`, `NaN`, and malformed strings.
|
|
127
|
+
- Defensive error handling when accessing live settings scopes.
|
|
128
|
+
|
|
115
129
|
## 🚀 Changed in v0.1.5
|
|
116
130
|
|
|
117
131
|
* **Strict `settings.plugin.item` Registration (#3)**:
|
package/README.ru.md
CHANGED
|
@@ -112,6 +112,20 @@ graph LR
|
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
|
|
115
|
+
## 🚀 Изменения в версии v0.1.6 (Changed in v0.1.6)
|
|
116
|
+
|
|
117
|
+
* **Единый стиль по референсу `dsh-clinebot` (#6)**:
|
|
118
|
+
- Идемпотентный вызов `ensureCss()` вынесен за пределы цикла рендера (`#dsh-usage-guard-full-css` с атрибутом `data-dsh-plugin="dsh-usage-guard"`).
|
|
119
|
+
- Карточка настроек защищена компонентом `ErrorBoundary`: ошибки интерфейса не роняют панель настроек DSH и сопровождаются кнопкой «Retry».
|
|
120
|
+
- Полное использование токенов темы DSH для карточек, полей и бейджей (`.ug-section-card`, `.ug-field-card`, `.ug-stat-box`, `.ug-badge-ok`, `.ug-badge-warn`, `.ug-btn-primary`).
|
|
121
|
+
- Добавлен блок телеметрии статуса защиты и целевой службы `sessionProjections`.
|
|
122
|
+
- Внедрена функция локализации `makeT(ru, en)` с поддержкой интерполяции переменных `{var}` и fallback.
|
|
123
|
+
- Фоновая синхронизация зеркала настроек `refreshMirrorUntilVisible(ctx)` с безопасным unref-таймером.
|
|
124
|
+
* **Аудит стабильности и расширение синонимов расхода**:
|
|
125
|
+
- Добавлена поддержка вложенных структур кэша OpenAI (`prompt_tokens_details.cachedTokens`, `prompt_tokens_details.cacheCreationTokens`).
|
|
126
|
+
- Усилена функция приведения чисел (`coerceNumber`) против `Infinity`, `-Infinity`, `NaN` и мусорных строк.
|
|
127
|
+
- Безопасная обработка настроек ядра и перехват исключений хоста.
|
|
128
|
+
|
|
115
129
|
## 🚀 Изменения в версии v0.1.5 (Changed in v0.1.5)
|
|
116
130
|
|
|
117
131
|
* **Строгая регистрация в `settings.plugin.item` (#3)**:
|
package/lib/client.js
CHANGED
|
@@ -1,106 +1,252 @@
|
|
|
1
|
+
// Plugin Settings Card (settings.plugin.item).
|
|
1
2
|
window.__ModuleLoader__.load({
|
|
2
3
|
id: '@goodandready/dsh-usage-guard',
|
|
3
4
|
factory: (require) => {
|
|
4
|
-
|
|
5
|
+
const module = { exports: {} };
|
|
5
6
|
const React = require('react');
|
|
6
7
|
const NS = 'dsh-usage-guard';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const en = {
|
|
10
|
+
title: 'Usage Guard',
|
|
11
|
+
subtitle: 'Token-usage sanitizer & session crash protection',
|
|
12
|
+
header_desc: 'Guards session history projections from corrupted or missing token counters across all LLM providers.',
|
|
13
|
+
badge_active: 'Active Guard',
|
|
14
|
+
badge_report_only: 'Report Only',
|
|
15
|
+
'settings.loading': 'Loading Usage Guard settings…',
|
|
16
|
+
'settings.unavailable': 'Settings scope unavailable (host namespace is not ready).',
|
|
17
|
+
'settings.retry': 'Retry',
|
|
18
|
+
saved: 'Settings saved successfully',
|
|
19
|
+
save: 'Save Changes',
|
|
20
|
+
saving: 'Saving…',
|
|
21
|
+
repair: 'Auto-repair token counters',
|
|
22
|
+
repairDesc: 'Replace missing, non-numeric, or malformed counters with 0 before summing to prevent NaN history lockups.',
|
|
23
|
+
report: 'Log diagnostic warnings',
|
|
24
|
+
reportDesc: 'Log a detailed warning naming the session turn, step, and raw sample whenever damaged usage arrives.',
|
|
25
|
+
stats_title: 'Protection Status',
|
|
26
|
+
stats_mode: 'Current Mode',
|
|
27
|
+
stats_mode_repair: 'Active Repair (Safe)',
|
|
28
|
+
stats_mode_report: 'Audit Only (Unsafe)',
|
|
29
|
+
stats_scope: 'Scope Target',
|
|
30
|
+
stats_scope_val: 'sessionProjections',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ru = {
|
|
34
|
+
title: 'Usage Guard',
|
|
35
|
+
subtitle: 'Санитайзер расхода токенов и защита от сбоев сессий',
|
|
36
|
+
header_desc: 'Защищает проекции истории сессий от повреждённых или отсутствующих счётчиков токенов всех LLM-провайдеров.',
|
|
37
|
+
badge_active: 'Защита активна',
|
|
38
|
+
badge_report_only: 'Только аудит',
|
|
39
|
+
'settings.loading': 'Загрузка настроек Usage Guard…',
|
|
40
|
+
'settings.unavailable': 'Настройки недоступны (пространство хоста ещё не готово).',
|
|
41
|
+
'settings.retry': 'Повторить попытку',
|
|
42
|
+
saved: 'Настройки успешно сохранены',
|
|
43
|
+
save: 'Сохранить изменения',
|
|
44
|
+
saving: 'Сохранение…',
|
|
45
|
+
repair: 'Автоматическая починка счётчиков',
|
|
46
|
+
repairDesc: 'Подставлять 0 вместо пропущенных или нечисловых счётчиков до сложения ядра, предотвращая зависание истории с ошибкой NaN.',
|
|
47
|
+
report: 'Диагностические предупреждения',
|
|
48
|
+
reportDesc: 'Выводить в системный журнал подробную строку с указанием сессии, хода, шага и сырой порции при обнаружении повреждений.',
|
|
49
|
+
stats_title: 'Статус защиты',
|
|
50
|
+
stats_mode: 'Текущий режим',
|
|
51
|
+
stats_mode_repair: 'Активная починка (Безопасно)',
|
|
52
|
+
stats_mode_report: 'Только аудит (Небезопасно)',
|
|
53
|
+
stats_scope: 'Целевая служба',
|
|
54
|
+
stats_scope_val: 'sessionProjections',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function makeT(dict, fallback) {
|
|
58
|
+
return function t(key, vars) {
|
|
59
|
+
let val = (dict && dict[key]) || (fallback && fallback[key]) || key;
|
|
60
|
+
if (vars && typeof val === 'string') {
|
|
61
|
+
for (const k of Object.keys(vars)) {
|
|
62
|
+
val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return val;
|
|
66
|
+
};
|
|
14
67
|
}
|
|
15
68
|
|
|
16
|
-
function FallbackChevron(
|
|
17
|
-
return React.createElement(
|
|
18
|
-
|
|
19
|
-
height: 14,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
69
|
+
function FallbackChevron() {
|
|
70
|
+
return React.createElement(
|
|
71
|
+
'svg',
|
|
72
|
+
{ width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
|
|
73
|
+
React.createElement('path', {
|
|
74
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
75
|
+
stroke: 'currentColor',
|
|
76
|
+
strokeWidth: 1.5,
|
|
77
|
+
strokeLinecap: 'round',
|
|
78
|
+
strokeLinejoin: 'round',
|
|
79
|
+
})
|
|
80
|
+
);
|
|
28
81
|
}
|
|
29
|
-
const Chevron =
|
|
82
|
+
const Chevron = FallbackChevron;
|
|
83
|
+
|
|
84
|
+
function ensureCss() {
|
|
85
|
+
if (typeof document === 'undefined') return;
|
|
86
|
+
if (document.getElementById('dsh-usage-guard-full-css')) return;
|
|
87
|
+
const style = document.createElement('style');
|
|
88
|
+
style.id = 'dsh-usage-guard-full-css';
|
|
89
|
+
style.dataset.dshPlugin = NS;
|
|
90
|
+
style.textContent = `
|
|
91
|
+
.ug-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:14px;list-style:none}
|
|
92
|
+
.ug-head-btn{background:none;border:none;cursor:pointer;display:flex;align-items:center;width:100%;padding:0;text-align:left}
|
|
93
|
+
.ug-head-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
94
|
+
.ug-head-sub{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:2px}
|
|
95
|
+
|
|
96
|
+
.ug-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
|
|
97
|
+
.ug-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}
|
|
98
|
+
.ug-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
|
|
99
|
+
.ug-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}
|
|
100
|
+
|
|
101
|
+
.ug-body{display:flex;flex-direction:column;gap:14px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:16px;margin-top:4px}
|
|
102
|
+
.ug-field-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}
|
|
103
|
+
.ug-row{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer}
|
|
104
|
+
.ug-row-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
105
|
+
.ug-field-desc{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
|
|
106
|
+
|
|
107
|
+
.ug-checkbox{width:18px;height:18px;accent-color:var(--dsw-alias-state-brand-primary, #6366f1);cursor:pointer}
|
|
108
|
+
|
|
109
|
+
.ug-stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:10px}
|
|
110
|
+
.ug-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:2px}
|
|
111
|
+
.ug-stat-val{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
112
|
+
.ug-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
113
|
+
|
|
114
|
+
.ug-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 16px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
|
|
115
|
+
.ug-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
|
|
116
|
+
.ug-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
117
|
+
.ug-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
118
|
+
.ug-btn:disabled{opacity:0.5;cursor:not-allowed}
|
|
30
119
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
120
|
+
.ug-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid rgba(16,185,129,0.2)}
|
|
121
|
+
.ug-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid rgba(239,68,68,0.2)}
|
|
122
|
+
`;
|
|
123
|
+
document.head.appendChild(style);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createErrorBoundary() {
|
|
127
|
+
if (!React || typeof React.Component !== 'function') {
|
|
128
|
+
return function NoopBoundary(props) { return props?.children || null; };
|
|
129
|
+
}
|
|
130
|
+
return class ErrorBoundary extends React.Component {
|
|
131
|
+
constructor(props) {
|
|
132
|
+
super(props);
|
|
133
|
+
this.state = { hasError: false, error: null };
|
|
134
|
+
}
|
|
135
|
+
static getDerivedStateFromError(error) {
|
|
136
|
+
return { hasError: true, error };
|
|
137
|
+
}
|
|
138
|
+
componentDidCatch(error, errorInfo) {
|
|
139
|
+
console.error('[dsh-usage-guard] UI Error:', error, errorInfo);
|
|
140
|
+
}
|
|
141
|
+
render() {
|
|
142
|
+
if (this.state.hasError) {
|
|
143
|
+
return React.createElement(
|
|
144
|
+
'div',
|
|
145
|
+
{ className: 'ug-alert-bad', style: { margin: '12px 0' } },
|
|
146
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '4px' } }, '⚠️ Usage Guard UI Error:'),
|
|
147
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
148
|
+
React.createElement(
|
|
149
|
+
'button',
|
|
150
|
+
{
|
|
151
|
+
type: 'button',
|
|
152
|
+
className: 'ug-btn',
|
|
153
|
+
style: { marginTop: '8px', fontSize: '12px', padding: '4px 10px' },
|
|
154
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
155
|
+
},
|
|
156
|
+
'Retry'
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return this.props?.children || null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const ErrorBoundary = createErrorBoundary();
|
|
165
|
+
|
|
166
|
+
function SettingsPage({ ctx, t }) {
|
|
167
|
+
const [snap, setSnap] = React.useState({ status: 'loading', value: {} });
|
|
34
168
|
const [draft, setDraft] = React.useState({ repair: true, report: true });
|
|
35
169
|
const [saving, setSaving] = React.useState(false);
|
|
36
|
-
const [msg, setMsg] = React.useState('');
|
|
37
|
-
|
|
38
|
-
const initializedRef = React.useRef(false);
|
|
170
|
+
const [msg, setMsg] = React.useState({ text: '', ok: true });
|
|
39
171
|
const isDirtyRef = React.useRef(false);
|
|
40
172
|
const msgTimerRef = React.useRef(null);
|
|
41
173
|
|
|
42
174
|
React.useEffect(() => {
|
|
43
|
-
|
|
44
|
-
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
45
|
-
};
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
React.useEffect(() => {
|
|
49
|
-
let off = null;
|
|
175
|
+
let active = true;
|
|
50
176
|
try {
|
|
51
|
-
const scope = ctx
|
|
177
|
+
const scope = ctx?.settingsScope && typeof ctx.settingsScope.bind === 'function'
|
|
178
|
+
? ctx.settingsScope.bind({ namespace: NS })
|
|
179
|
+
: null;
|
|
52
180
|
if (!scope) {
|
|
53
|
-
setSnap({ status: 'unavailable', value:
|
|
54
|
-
return;
|
|
181
|
+
setSnap({ status: 'unavailable', value: {} });
|
|
182
|
+
return () => {};
|
|
55
183
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
184
|
+
const read = async () => {
|
|
185
|
+
try {
|
|
186
|
+
const res = await scope.get();
|
|
187
|
+
if (active && res && typeof res === 'object') {
|
|
188
|
+
setSnap({ status: 'ready', value: res });
|
|
189
|
+
if (!isDirtyRef.current) {
|
|
190
|
+
setDraft({
|
|
191
|
+
repair: res.repair !== false,
|
|
192
|
+
report: res.report !== false,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (_) {
|
|
197
|
+
if (active) setSnap({ status: 'unavailable', value: {} });
|
|
65
198
|
}
|
|
66
199
|
};
|
|
67
|
-
|
|
68
|
-
|
|
200
|
+
read();
|
|
201
|
+
const unwatch = typeof scope.watch === 'function'
|
|
202
|
+
? scope.watch((next) => {
|
|
203
|
+
if (!active || !next || typeof next !== 'object') return;
|
|
204
|
+
setSnap({ status: 'ready', value: next });
|
|
205
|
+
if (!isDirtyRef.current) {
|
|
206
|
+
setDraft({
|
|
207
|
+
repair: next.repair !== false,
|
|
208
|
+
report: next.report !== false,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
: () => {};
|
|
213
|
+
return () => {
|
|
214
|
+
active = false;
|
|
215
|
+
try { unwatch(); } catch (_) {}
|
|
216
|
+
};
|
|
69
217
|
} catch (_) {
|
|
70
|
-
setSnap({ status: 'unavailable', value:
|
|
218
|
+
setSnap({ status: 'unavailable', value: {} });
|
|
219
|
+
return () => {};
|
|
71
220
|
}
|
|
72
|
-
return () => {
|
|
73
|
-
try { off && off(); } catch {}
|
|
74
|
-
};
|
|
75
221
|
}, [ctx]);
|
|
76
222
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
})();
|
|
223
|
+
React.useEffect(() => {
|
|
224
|
+
return () => {
|
|
225
|
+
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
226
|
+
};
|
|
227
|
+
}, []);
|
|
84
228
|
|
|
85
|
-
const showMessage = (text,
|
|
229
|
+
const showMessage = (text, ok = true) => {
|
|
86
230
|
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
87
|
-
setMsg(text);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
231
|
+
setMsg({ text, ok });
|
|
232
|
+
msgTimerRef.current = setTimeout(() => {
|
|
233
|
+
setMsg({ text: '', ok: true });
|
|
234
|
+
}, 4000);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const handleToggle = (field, checked) => {
|
|
238
|
+
isDirtyRef.current = true;
|
|
239
|
+
if (msg.text) {
|
|
240
|
+
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
241
|
+
setMsg({ text: '', ok: true });
|
|
92
242
|
}
|
|
243
|
+
setDraft((prev) => ({ ...prev, [field]: checked }));
|
|
93
244
|
};
|
|
94
245
|
|
|
95
246
|
const onSave = async () => {
|
|
247
|
+
if (!ctx?.settingsScope || typeof ctx.settingsScope.bind !== 'function') return;
|
|
96
248
|
setSaving(true);
|
|
97
|
-
setMsg('');
|
|
98
249
|
try {
|
|
99
|
-
if (!ctx.settingsScope || !ctx.settingsScope.bind) {
|
|
100
|
-
showMessage('settingsScope unavailable', false);
|
|
101
|
-
setSaving(false);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
250
|
const scope = ctx.settingsScope.bind({ namespace: NS });
|
|
105
251
|
const errs = [];
|
|
106
252
|
for (const k of ['repair', 'report']) {
|
|
@@ -114,7 +260,7 @@ window.__ModuleLoader__.load({
|
|
|
114
260
|
showMessage(errs.join('; '), false);
|
|
115
261
|
} else {
|
|
116
262
|
isDirtyRef.current = false;
|
|
117
|
-
showMessage(t('saved')
|
|
263
|
+
showMessage(t('saved'), true);
|
|
118
264
|
}
|
|
119
265
|
} catch (e) {
|
|
120
266
|
showMessage(String(e && e.message ? e.message : e), false);
|
|
@@ -122,130 +268,288 @@ window.__ModuleLoader__.load({
|
|
|
122
268
|
setSaving(false);
|
|
123
269
|
};
|
|
124
270
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
271
|
+
if (snap.status === 'loading') {
|
|
272
|
+
return React.createElement(
|
|
273
|
+
'div',
|
|
274
|
+
{ className: 'ug-field-card', style: { color: 'var(--dsw-alias-label-secondary)' } },
|
|
275
|
+
t('settings.loading')
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (snap.status === 'unavailable') {
|
|
280
|
+
return React.createElement(
|
|
281
|
+
'div',
|
|
282
|
+
{ className: 'ug-alert-bad' },
|
|
283
|
+
t('settings.unavailable')
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const isRepair = draft.repair !== false;
|
|
288
|
+
|
|
289
|
+
return React.createElement(
|
|
290
|
+
'div',
|
|
291
|
+
{ className: 'ug-body' },
|
|
133
292
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
React.createElement(
|
|
164
|
-
|
|
165
|
-
|
|
293
|
+
// Telemetry / Protection status banner
|
|
294
|
+
React.createElement(
|
|
295
|
+
'div',
|
|
296
|
+
{ className: 'ug-stats-grid' },
|
|
297
|
+
React.createElement(
|
|
298
|
+
'div',
|
|
299
|
+
{ className: 'ug-stat-box' },
|
|
300
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('stats_mode')),
|
|
301
|
+
React.createElement(
|
|
302
|
+
'span',
|
|
303
|
+
{
|
|
304
|
+
className: 'ug-stat-val',
|
|
305
|
+
style: { color: isRepair ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warning-primary)' },
|
|
306
|
+
},
|
|
307
|
+
isRepair ? t('stats_mode_repair') : t('stats_mode_report')
|
|
308
|
+
)
|
|
309
|
+
),
|
|
310
|
+
React.createElement(
|
|
311
|
+
'div',
|
|
312
|
+
{ className: 'ug-stat-box' },
|
|
313
|
+
React.createElement('span', { className: 'ug-stat-lbl' }, t('stats_scope')),
|
|
314
|
+
React.createElement('span', { className: 'ug-stat-val' }, t('stats_scope_val'))
|
|
315
|
+
)
|
|
316
|
+
),
|
|
317
|
+
|
|
318
|
+
// Field 1: repair
|
|
319
|
+
React.createElement(
|
|
320
|
+
'div',
|
|
321
|
+
{ className: 'ug-field-card' },
|
|
322
|
+
React.createElement(
|
|
323
|
+
'label',
|
|
324
|
+
{ className: 'ug-row', htmlFor: 'ug-repair-toggle' },
|
|
325
|
+
React.createElement('span', { className: 'ug-row-title' }, t('repair')),
|
|
326
|
+
React.createElement('input', {
|
|
327
|
+
id: 'ug-repair-toggle',
|
|
328
|
+
type: 'checkbox',
|
|
329
|
+
className: 'ug-checkbox',
|
|
330
|
+
checked: !!draft.repair,
|
|
331
|
+
onChange: (e) => handleToggle('repair', e.target.checked),
|
|
332
|
+
})
|
|
333
|
+
),
|
|
334
|
+
React.createElement('div', { className: 'ug-field-desc' }, t('repairDesc'))
|
|
166
335
|
),
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
),
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
})
|
|
194
|
-
),
|
|
195
|
-
React.createElement('div', { className: 'ug-sub' }, t('reportDesc') || 'Log a line naming the turn, step, and raw sample whenever damaged usage arrives')
|
|
196
|
-
),
|
|
197
|
-
React.createElement('div', { className: 'ug-foot' },
|
|
198
|
-
msg ? React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginRight: 'auto' } }, msg) : null,
|
|
199
|
-
React.createElement('button', {
|
|
200
|
-
className: 'ug-save',
|
|
201
|
-
onClick: onSave,
|
|
202
|
-
disabled: saving,
|
|
203
|
-
}, saving ? (t('saving') || 'Saving…') : (t('save') || 'Save'))
|
|
336
|
+
|
|
337
|
+
// Field 2: report
|
|
338
|
+
React.createElement(
|
|
339
|
+
'div',
|
|
340
|
+
{ className: 'ug-field-card' },
|
|
341
|
+
React.createElement(
|
|
342
|
+
'label',
|
|
343
|
+
{ className: 'ug-row', htmlFor: 'ug-report-toggle' },
|
|
344
|
+
React.createElement('span', { className: 'ug-row-title' }, t('report')),
|
|
345
|
+
React.createElement('input', {
|
|
346
|
+
id: 'ug-report-toggle',
|
|
347
|
+
type: 'checkbox',
|
|
348
|
+
className: 'ug-checkbox',
|
|
349
|
+
checked: !!draft.report,
|
|
350
|
+
onChange: (e) => handleToggle('report', e.target.checked),
|
|
351
|
+
})
|
|
352
|
+
),
|
|
353
|
+
React.createElement('div', { className: 'ug-field-desc' }, t('reportDesc'))
|
|
354
|
+
),
|
|
355
|
+
|
|
356
|
+
// Feedback message
|
|
357
|
+
msg.text
|
|
358
|
+
? React.createElement(
|
|
359
|
+
'div',
|
|
360
|
+
{ className: msg.ok ? 'ug-alert-ok' : 'ug-alert-bad' },
|
|
361
|
+
msg.text
|
|
204
362
|
)
|
|
205
|
-
|
|
206
|
-
|
|
363
|
+
: null,
|
|
364
|
+
|
|
365
|
+
// Actions
|
|
366
|
+
React.createElement(
|
|
367
|
+
'div',
|
|
368
|
+
{ style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' } },
|
|
369
|
+
React.createElement(
|
|
370
|
+
'button',
|
|
371
|
+
{
|
|
372
|
+
type: 'button',
|
|
373
|
+
className: 'ug-btn ug-btn-primary',
|
|
374
|
+
onClick: onSave,
|
|
375
|
+
disabled: saving,
|
|
376
|
+
},
|
|
377
|
+
saving ? t('saving') : t('save')
|
|
378
|
+
)
|
|
379
|
+
)
|
|
207
380
|
);
|
|
208
381
|
}
|
|
209
382
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
383
|
+
// Accordion PluginCard for settings.plugin.item
|
|
384
|
+
function PluginCard(props) {
|
|
385
|
+
const [open, setOpen] = React.useState(false);
|
|
386
|
+
const t = props?.t || makeT(ru, en);
|
|
387
|
+
|
|
388
|
+
React.useEffect(() => {
|
|
389
|
+
ensureCss();
|
|
390
|
+
}, []);
|
|
391
|
+
|
|
392
|
+
const isRepairActive = props?.repair !== false;
|
|
393
|
+
const badgeText = isRepairActive ? t('badge_active') : t('badge_report_only');
|
|
394
|
+
const badgeClass = isRepairActive ? 'ug-badge ug-badge-ok' : 'ug-badge ug-badge-warn';
|
|
395
|
+
|
|
396
|
+
return React.createElement(
|
|
397
|
+
'li',
|
|
398
|
+
{ className: 'ug-section-card' },
|
|
399
|
+
React.createElement(
|
|
400
|
+
'button',
|
|
401
|
+
{
|
|
402
|
+
type: 'button',
|
|
403
|
+
className: 'ug-head-btn',
|
|
404
|
+
'aria-expanded': open,
|
|
405
|
+
onClick: () => setOpen((v) => !v),
|
|
228
406
|
},
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
407
|
+
React.createElement(
|
|
408
|
+
'div',
|
|
409
|
+
{ style: { flex: 1, paddingRight: '12px' } },
|
|
410
|
+
React.createElement('div', { className: 'ug-head-title' }, t('title')),
|
|
411
|
+
React.createElement('div', { className: 'ug-head-sub' }, t('subtitle'))
|
|
412
|
+
),
|
|
413
|
+
React.createElement('span', { className: badgeClass, style: { marginRight: '12px' } }, badgeText),
|
|
414
|
+
React.createElement(
|
|
415
|
+
'span',
|
|
237
416
|
{
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
417
|
+
style: {
|
|
418
|
+
transform: open ? 'rotate(180deg)' : 'none',
|
|
419
|
+
transition: 'transform .16s ease',
|
|
420
|
+
display: 'inline-flex',
|
|
421
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
422
|
+
},
|
|
242
423
|
},
|
|
243
|
-
|
|
244
|
-
)
|
|
424
|
+
React.createElement(Chevron)
|
|
425
|
+
)
|
|
426
|
+
),
|
|
427
|
+
open
|
|
428
|
+
? React.createElement(
|
|
429
|
+
ErrorBoundary,
|
|
430
|
+
null,
|
|
431
|
+
React.createElement(SettingsPage, {
|
|
432
|
+
...props,
|
|
433
|
+
ctx: (props && props.ctx) || ctx,
|
|
434
|
+
t,
|
|
435
|
+
})
|
|
436
|
+
)
|
|
437
|
+
: null
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function refreshMirrorUntilVisible(ctx) {
|
|
442
|
+
const visible = () => {
|
|
443
|
+
try {
|
|
444
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope;
|
|
445
|
+
const view = s?.describe?.()?.getSnapshot?.()?.view;
|
|
446
|
+
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS);
|
|
447
|
+
} catch (_) {
|
|
448
|
+
return false;
|
|
245
449
|
}
|
|
246
|
-
}
|
|
247
|
-
|
|
450
|
+
};
|
|
451
|
+
if (visible()) return () => {};
|
|
452
|
+
let tries = 0;
|
|
453
|
+
const timer = setInterval(() => {
|
|
454
|
+
if (visible() || tries >= 15) {
|
|
455
|
+
clearInterval(timer);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
tries += 1;
|
|
459
|
+
try {
|
|
460
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope;
|
|
461
|
+
s?.describe?.()?.load?.();
|
|
462
|
+
} catch (_) {}
|
|
463
|
+
}, 1000);
|
|
464
|
+
if (timer && typeof timer.unref === 'function') {
|
|
465
|
+
timer.unref();
|
|
466
|
+
}
|
|
467
|
+
return () => clearInterval(timer);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let ctx = null;
|
|
471
|
+
|
|
472
|
+
function apply(c) {
|
|
473
|
+
ctx = c;
|
|
474
|
+
const addLocale = (locale, dictionary) => {
|
|
475
|
+
try {
|
|
476
|
+
return ctx.locale.register(NS, locale, dictionary);
|
|
477
|
+
} catch (_) {
|
|
478
|
+
return () => {};
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
483
|
+
if (typeof ctx.effect === 'function') {
|
|
484
|
+
ctx.effect(() => {
|
|
485
|
+
const undo = [addLocale('en', en), addLocale('ru', ru)];
|
|
486
|
+
return () => {
|
|
487
|
+
for (const off of undo) off();
|
|
488
|
+
};
|
|
489
|
+
}, 'dsh-usage-guard: dictionaries');
|
|
490
|
+
} else {
|
|
491
|
+
addLocale('en', en);
|
|
492
|
+
addLocale('ru', ru);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
248
495
|
|
|
496
|
+
if (typeof ctx.effect === 'function') {
|
|
497
|
+
ctx.effect(
|
|
498
|
+
() => refreshMirrorUntilVisible(ctx),
|
|
499
|
+
'dsh-usage-guard: re-read the settings mirror until namespace appears',
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function registerSlotWhenReady(slotName, registerFn) {
|
|
504
|
+
if (!ctx.slots) return;
|
|
505
|
+
if (typeof ctx.slots.inject === 'function') {
|
|
506
|
+
try {
|
|
507
|
+
ctx.slots.inject(slotName, () => {
|
|
508
|
+
try {
|
|
509
|
+
return registerFn();
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.warn('[dsh-usage-guard] Error registering slot ' + slotName + ':', err);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.warn('[dsh-usage-guard] Failed to inject slot ' + slotName + ':', err);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (typeof ctx.slots.register === 'function') {
|
|
520
|
+
try {
|
|
521
|
+
registerFn();
|
|
522
|
+
} catch (err) {
|
|
523
|
+
console.warn('[dsh-usage-guard] Failed direct registration for ' + slotName + ':', err);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
registerSlotWhenReady('settings.plugin.item', () =>
|
|
529
|
+
ctx.slots.register(
|
|
530
|
+
{
|
|
531
|
+
name: 'settings.plugin.item',
|
|
532
|
+
key: NS,
|
|
533
|
+
locale: NS,
|
|
534
|
+
inject: () => ({ ctx }),
|
|
535
|
+
},
|
|
536
|
+
(props) =>
|
|
537
|
+
React.createElement(
|
|
538
|
+
ErrorBoundary,
|
|
539
|
+
null,
|
|
540
|
+
React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx })
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
module.exports = {
|
|
547
|
+
apply,
|
|
548
|
+
inject: ['slots', 'locale', 'settingsScope'],
|
|
549
|
+
makeT,
|
|
550
|
+
ensureCss,
|
|
551
|
+
createErrorBoundary,
|
|
552
|
+
};
|
|
249
553
|
return module.exports;
|
|
250
554
|
},
|
|
251
|
-
});
|
|
555
|
+
});
|
package/lib/index.js
CHANGED
|
@@ -52,13 +52,20 @@ export function apply(ctx, config) {
|
|
|
52
52
|
try {
|
|
53
53
|
live = Config(structuredClone(config ?? {})) ?? config
|
|
54
54
|
} catch (_) {
|
|
55
|
-
live = Config({}) ?? {}
|
|
55
|
+
live = Config({}) ?? { repair: true, report: true }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
ctx.inject(['settings'], (sctx) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
try {
|
|
60
|
+
const scope = sctx.settings.register(name, Config, { base: config })
|
|
61
|
+
try {
|
|
62
|
+
const fresh = scope.get()
|
|
63
|
+
if (fresh && typeof fresh === 'object') live = fresh
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
sctx.effect(() => scope.watch((next) => {
|
|
66
|
+
if (next && typeof next === 'object') live = next
|
|
67
|
+
}))
|
|
68
|
+
} catch (_) {}
|
|
62
69
|
})
|
|
63
70
|
|
|
64
71
|
// О каждом сочетании сессии, хода, шага и набора полей жалуемся один раз:
|
|
@@ -90,7 +97,7 @@ export function apply(ctx, config) {
|
|
|
90
97
|
return event
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
if (live.report !== false) {
|
|
100
|
+
if (live && live.report !== false) {
|
|
94
101
|
const sessionTag = event?.sessionId ?? event?.data?.sessionId ?? event?.data?.turn ?? 'global'
|
|
95
102
|
const seen = `${String(sessionTag)}/${String(found.turn)}/${String(found.step)}/${bad.join(',')}`
|
|
96
103
|
if (!told.has(seen)) {
|
|
@@ -108,7 +115,7 @@ export function apply(ctx, config) {
|
|
|
108
115
|
}
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
const result = live.repair === false ? event : healed(event, bad)
|
|
118
|
+
const result = live && live.repair === false ? event : healed(event, bad)
|
|
112
119
|
eventCache.set(event, result)
|
|
113
120
|
return result
|
|
114
121
|
}
|
|
@@ -119,4 +126,4 @@ export function apply(ctx, config) {
|
|
|
119
126
|
'dsh-usage-guard: обезвреживание порций расхода',
|
|
120
127
|
)
|
|
121
128
|
})
|
|
122
|
-
}
|
|
129
|
+
}
|
package/lib/usage.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Как устроена порция расхода в событиях сессии.
|
|
2
2
|
//
|
|
3
|
-
// Харнесс
|
|
3
|
+
// Харнесс слушает шину событий сессии и собирает статистику по двум типам
|
|
4
|
+
// записей:
|
|
5
|
+
//
|
|
6
|
+
// 1. Потоковые порции ответа ассистента:
|
|
7
|
+
// `assistant/chunk` -> `event.data.chunk.usage`
|
|
8
|
+
//
|
|
9
|
+
// 2. Финальное сообщение ассистента:
|
|
10
|
+
// `assistant/message` -> `event.data.usage`
|
|
11
|
+
//
|
|
12
|
+
// В обоих случаях ядро ждёт четыре поля:
|
|
4
13
|
//
|
|
5
14
|
// uncachedInputTokens: usage.inputTokens,
|
|
6
15
|
// outputTokens: usage.outputTokens,
|
|
7
16
|
// cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
8
17
|
// cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
9
18
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// Из-за этого одна битая порция валит не просто
|
|
19
|
+
// Если первые два поля пропущены, не являются числом или содержат нечисловую строку,
|
|
20
|
+
// их сложение с нулём даёт NaN. Из-за этого одна битая порция валит не просто
|
|
13
21
|
// счётчик, а всю историю сессии.
|
|
14
22
|
export const FIELDS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']
|
|
15
23
|
|
|
@@ -65,6 +73,7 @@ function sound(value) {
|
|
|
65
73
|
/**
|
|
66
74
|
* Безопасное приведение значения к корректному неотрицательному целому числу.
|
|
67
75
|
* Округляет дробные числа и парсит числовые строки (в т.ч. с десятичной точкой).
|
|
76
|
+
* Исключает NaN, Infinity, отрицательные величины и мусорные строки.
|
|
68
77
|
*/
|
|
69
78
|
function coerceNumber(value) {
|
|
70
79
|
if (typeof value === 'number') {
|
|
@@ -134,14 +143,21 @@ export function borrowed(usage, field) {
|
|
|
134
143
|
if (num !== undefined) return num
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
// 2. Поиск во вложенных деталях кэша (OpenAI-style: prompt_tokens_details.cached_tokens)
|
|
146
|
+
// 2. Поиск во вложенных деталях кэша (OpenAI-style: prompt_tokens_details.cached_tokens / cachedTokens)
|
|
138
147
|
if (field === 'cacheReadTokens' && usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
|
|
139
148
|
const details = usage.prompt_tokens_details
|
|
140
|
-
const num = coerceNumber(details.cached_tokens ?? details.cachedTokens)
|
|
149
|
+
const num = coerceNumber(details.cached_tokens ?? details.cachedTokens ?? details.cache_read_tokens)
|
|
150
|
+
if (num !== undefined) return num
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 3. Поиск во вложенных деталях создания кэша (cache_creation_input_tokens)
|
|
154
|
+
if (field === 'cacheWriteTokens' && usage.prompt_tokens_details && typeof usage.prompt_tokens_details === 'object') {
|
|
155
|
+
const details = usage.prompt_tokens_details
|
|
156
|
+
const num = coerceNumber(details.cache_creation_tokens ?? details.cacheCreationTokens)
|
|
141
157
|
if (num !== undefined) return num
|
|
142
158
|
}
|
|
143
159
|
|
|
144
|
-
//
|
|
160
|
+
// 4. Само поле, если оно было передано валидной строкой или нецелым числом (напр. "123" или 15.4)
|
|
145
161
|
const selfNum = coerceNumber(usage[field])
|
|
146
162
|
if (selfNum !== undefined) return selfNum
|
|
147
163
|
|
|
@@ -206,4 +222,4 @@ export function complaint(found, bad) {
|
|
|
206
222
|
.map((field) => (found.usage && borrowed(found.usage, field) !== undefined ? `${field} recovered via alias` : `${field} zeroed`))
|
|
207
223
|
.join('; ')
|
|
208
224
|
return `malformed usage sample — ${where}; received: ${safeStringify(found.usage)}; ${taken}`
|
|
209
|
-
}
|
|
225
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-usage-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Keeps a malformed token-usage sample from taking a whole session history down with it: a missing or non-numeric counter becomes zero before the harness adds it up, so the fold never yields NaN and already-broken sessions open again.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|