@goodandready/dsh-usage-guard 0.1.2 → 0.1.3
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 +25 -7
- package/README.ru.md +25 -7
- package/README.zh.md +23 -5
- package/lib/client.js +42 -10
- package/lib/index.js +24 -5
- package/lib/usage.js +34 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,23 +92,41 @@ Before substituting zero, `dsh-usage-guard` scans an extensive dictionary of ind
|
|
|
92
92
|
| `cacheReadTokens` | `cache_read_tokens`, `cachedTokens`, `cached_tokens`, `cache_read_input_tokens`, `cachedContentTokenCount`, `prompt_tokens_details.cached_tokens` |
|
|
93
93
|
| `cacheWriteTokens` | `cache_write_tokens`, `cacheCreationTokens`, `cache_creation_input_tokens` |
|
|
94
94
|
|
|
95
|
-
### 3. Finite Non-Negative Soundness Validation (`sound`)
|
|
96
|
-
Strictly validates `typeof value === 'number' && Number.isFinite(value) && value >= 0` to filter out `NaN`, `Infinity`, `null`, `undefined`, negative error codes (e.g. `-1`), and malformed strings
|
|
95
|
+
### 3. Finite Non-Negative Integer Soundness Validation (`sound`)
|
|
96
|
+
Strictly validates `typeof value === 'number' && Number.isFinite(value) && value >= 0 && Number.isInteger(value)` to filter out `NaN`, `Infinity`, `null`, `undefined`, negative error codes (e.g. `-1`), non-integer floats, and malformed strings.
|
|
97
97
|
|
|
98
|
-
### 4. Safe Zero Fallback (`repaired`)
|
|
99
|
-
If a counter cannot be resolved from aliases, it is safely initialized to `0`.
|
|
98
|
+
### 4. Safe Zero Fallback & Float Rounding (`repaired`)
|
|
99
|
+
If a counter cannot be resolved from aliases, it is safely initialized to `0`. Fractional tokens or decimal strings are safely rounded via `Math.round()`, strictly satisfying the core DSH contract `z.number().int().nonnegative()`.
|
|
100
100
|
|
|
101
101
|
### 5. In-Memory Registry Monkey-Patching (`lib/patch.js`)
|
|
102
102
|
* **Pre-existing Projections**: Wraps all `.apply` methods currently registered in `sessionProjections.registrations` while preserving full `this` context.
|
|
103
103
|
* **Late-Binding Projections**: Traps future projection registrations via `map.set` wrapping, guaranteeing 100% coverage regardless of plugin loading order.
|
|
104
104
|
* **Universal Projection Protection**: Protects not only token counters, but also context pressure calculators and busy-state analyzers.
|
|
105
|
-
* **Zero Performance Overhead**: Uses shallow event cloning only along the usage path
|
|
105
|
+
* **Zero Performance Overhead**: Uses shallow event cloning only along the usage path, while a high-performance `WeakMap` cache ensures single execution across all 10–15 parallel DSH projections.
|
|
106
106
|
|
|
107
107
|
### 6. Deduplicated Diagnostic Reporting (`told`)
|
|
108
|
-
Logs informative diagnostic warnings naming the exact session, turn, step, raw payload, and recovery action (e.g. `inputTokens borrowed from alias` vs `inputTokens zeroed`). Incidents are deduplicated in memory so logs are not flooded during replays, and the cache is
|
|
108
|
+
Logs informative diagnostic warnings naming the exact session, turn, step, raw payload, and recovery action (e.g. `inputTokens borrowed from alias` vs `inputTokens zeroed`). Incidents are deduplicated in memory so logs are not flooded during replays, and the cache is bounded to 1,000 entries with O(1) FIFO eviction.
|
|
109
109
|
|
|
110
110
|
### 7. Native Web UI Settings Card (`lib/client.js`)
|
|
111
|
-
* Mounts into the native Settings tab under `Settings → Plugins → Plugin Settings` (`settings.plugin.item`) with real-time status badge and full localization.
|
|
111
|
+
* Mounts into the native Settings tab under `Settings → Plugins → Plugin Settings` (`settings.plugin.item`) with real-time status badge, auto-dismissing save feedback, and full localization.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🚀 Changed in v0.1.3
|
|
116
|
+
|
|
117
|
+
* **Fractional Token Protection (Floats & Decimals)**:
|
|
118
|
+
- DeepSeek Harness `@deepseek-ai/dsh-token-meter` projection schema enforces strict integers (`z.number().int().nonnegative()`). Fractional tokens (e.g. `42.5` or `"1540.2"` produced by routing proxies or weighted estimators) previously broke Zod schema validation.
|
|
119
|
+
- `sound()` now strictly validates `Number.isInteger(value)`.
|
|
120
|
+
- Floating-point numbers and decimal strings are now safely rounded to non-negative integers via `Math.round()` (`42.6` $\rightarrow$ `43`), protecting session history from schema rejections.
|
|
121
|
+
* **High-Performance WeakMap Cache (`guard`)**:
|
|
122
|
+
- DSH executes 10–15 parallel projection folds for every session event.
|
|
123
|
+
- A `WeakMap<event, guardedEvent>` cache sanitizes each incoming event exactly once on the first projection, returning the cached normalized reference to all subsequent projections in $O(1)$ without re-parsing or memory leak risks.
|
|
124
|
+
* **Strict O(1) FIFO Eviction in `told` Warning Cache**:
|
|
125
|
+
- Replaced bulk `told.clear()` with individual oldest key eviction `told.delete(oldest)` at 1,000 entries, maintaining continuous deduplication without sudden re-logging storms.
|
|
126
|
+
* **Web UI Settings Card UX & A11y Polish**:
|
|
127
|
+
- Save status confirmation ("Saved") now automatically auto-dismisses after 3 seconds and clears immediately upon toggle adjustment.
|
|
128
|
+
- Added reactive external synchronization with server-side config changes when the user has no uncommitted draft.
|
|
129
|
+
- Enhanced accessibility: connected inputs to labels via `htmlFor`/`id` and tagged the chevron icon with `aria-hidden="true"`.
|
|
112
130
|
|
|
113
131
|
---
|
|
114
132
|
|
package/README.ru.md
CHANGED
|
@@ -92,23 +92,41 @@ graph LR
|
|
|
92
92
|
| `cacheReadTokens` | `cache_read_tokens`, `cachedTokens`, `cached_tokens`, `cache_read_input_tokens`, `cachedContentTokenCount`, `prompt_tokens_details.cached_tokens` |
|
|
93
93
|
| `cacheWriteTokens` | `cache_write_tokens`, `cacheCreationTokens`, `cache_creation_input_tokens` |
|
|
94
94
|
|
|
95
|
-
### 3. Проверка на конечное неотрицательное число (`sound`)
|
|
96
|
-
Проверка `typeof value === 'number' && Number.isFinite(value) && value >= 0` гарантирует отсечение `NaN`, `Infinity`, `null`, `undefined`, отрицательных чисел (напр. `-1`
|
|
95
|
+
### 3. Проверка на конечное неотрицательное целое число (`sound`)
|
|
96
|
+
Проверка `typeof value === 'number' && Number.isFinite(value) && value >= 0 && Number.isInteger(value)` гарантирует отсечение `NaN`, `Infinity`, `null`, `undefined`, отрицательных чисел (напр. `-1`), нецелых дробей (floats) и строк.
|
|
97
97
|
|
|
98
|
-
### 4. Безопасная подстановка нуля (`repaired`)
|
|
99
|
-
Если число найти не удалось, подставляется `0`.
|
|
98
|
+
### 4. Безопасная подстановка нуля и округление (`repaired`)
|
|
99
|
+
Если число найти не удалось, подставляется `0`. Валидные дробные токены или числовые строки округляются до целых через `Math.round()`, строго удовлетворяя Zod-контракту ядра DSH `z.number().int().nonnegative()`.
|
|
100
100
|
|
|
101
101
|
### 5. Безопасный перехват в памяти (`lib/patch.js`)
|
|
102
102
|
* **Существующие проекции**: оборачивает все методы `.apply` в `sessionProjections.registrations` с полным сохранением контекста `this`.
|
|
103
103
|
* **Поздние проекции**: перехватывает будущие регистрации через хук `map.set`.
|
|
104
104
|
* **Комплексная защита**: защищает не только токены, но и формулы давления на контекст и разбор занятости.
|
|
105
|
-
* **Нулевой оверхед**: поверхностное клонирование применяется только к объекту
|
|
105
|
+
* **Нулевой оверхед**: поверхностное клонирование применяется только к объекту расхода, а `WeakMap` устраняет повторные вызовы при обходе 10–15 проекций DSH.
|
|
106
106
|
|
|
107
107
|
### 6. Дедуплицированное журналирование (`told`)
|
|
108
|
-
Выводит предупреждение с идентификатором сессии, номером хода, шагом и способом починки (взят по синониму или обнулён). Одинаковые инциденты не спамят в лог при повторных проигрываниях, а размер кэша ограничен 1000
|
|
108
|
+
Выводит предупреждение с идентификатором сессии, номером хода, шагом и способом починки (взят по синониму или обнулён). Одинаковые инциденты не спамят в лог при повторных проигрываниях, а размер кэша ограничен 1000 записями с честным O(1) FIFO-вытеснением для предотвращения утечек памяти.
|
|
109
109
|
|
|
110
110
|
### 7. Нативная карточка в Web UI (`lib/client.js`)
|
|
111
|
-
* Карточка настроек встроена во вкладку «Настройки → Плагины → Настройки плагинов» (`settings.plugin.item`) с динамическим бейджем
|
|
111
|
+
* Карточка настроек встроена во вкладку «Настройки → Плагины → Настройки плагинов» (`settings.plugin.item`) с динамическим бейджем статуса, плавно исчезающим подтверждением сохранения и полной локализацией.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🚀 Изменения в версии v0.1.3 (Changed in v0.1.3)
|
|
116
|
+
|
|
117
|
+
* **Защита от дробных токенов (Floats & Decimals)**:
|
|
118
|
+
- Схема валидации DSH ядра `@deepseek-ai/dsh-token-meter` требует строго целые числа (`z.number().int().nonnegative()`). Дробные токены (например, `42.5` или `"1540.2"` при усреднении или конвертации весов моделями) ранее приводили к падению схемы Zod.
|
|
119
|
+
- В v0.1.3 функция `sound()` строго требует `Number.isInteger(value)`.
|
|
120
|
+
- Все дробные числа и числовые строки с плавающей точкой теперь безопасно и корректно округляются через `Math.round()` (`42.6` $\rightarrow$ `43`), сохраняя читаемость истории сессий.
|
|
121
|
+
* **Высокопроизводительное WeakMap-кэширование (`guard`)**:
|
|
122
|
+
- В DSH активно 10–15 параллельных проекций, каждая из которых вызывала перехватчик `apply()`.
|
|
123
|
+
- Внедрён микрокэш на базе `WeakMap<event, guardedEvent>`. Санитизация события выполняется ровно 1 раз при первой проекции, а остальные 14 проекций получают уже нормализованное событие за $O(1)$ без лишнего клонирования и без риска утечек памяти.
|
|
124
|
+
* **Честное FIFO-вытеснение в кэше логов (`told`)**:
|
|
125
|
+
- Заменена полная очистка `told.clear()` на удаление старейшей записи `told.delete(oldest)` за $O(1)$ при достижении лимита в 1000 записей, что предотвращает повторные лавины логов при открытии старых диалогов.
|
|
126
|
+
* **Улучшение UX и доступности Web UI карточки настроек**:
|
|
127
|
+
- Сообщение об успешном сохранении («Сохранено») теперь плавно исчезает через 3 секунды и мгновенно сбрасывается при любом изменении переключателей.
|
|
128
|
+
- Добавлена реактивная синхронизация настроек с фоновыми изменениями сервера при нередактируемом драфте.
|
|
129
|
+
- Улучшена доступность: добавлены атрибуты `id` и `htmlFor` к полям формы, а иконке шеврона проставлен `aria-hidden="true"`.
|
|
112
130
|
|
|
113
131
|
---
|
|
114
132
|
|
package/README.zh.md
CHANGED
|
@@ -77,11 +77,29 @@ graph LR
|
|
|
77
77
|
|
|
78
78
|
1. **已损坏历史记录免修文件即刻复活**:不修改磁盘日志,在内存重放链路拦截修复;
|
|
79
79
|
2. **主流别名字典智能提取 (`borrowed`)**:覆盖 `prompt_tokens`、`completion_tokens`、`cached_tokens`、`promptTokenCount`、`prompt_eval_count` 等;
|
|
80
|
-
3.
|
|
81
|
-
4. **安全 0
|
|
82
|
-
5.
|
|
83
|
-
6.
|
|
84
|
-
7. **原生 Web UI 设置卡片 (`lib/client.js`)**:嵌入 DSH 原生设置中心(`settings.plugin.item
|
|
80
|
+
3. **严格非负整数有效性校验 (`sound`)**:剔除 `NaN`、`Infinity`、负数错误码(如 `-1`)、非整数浮点数与非法字符串;
|
|
81
|
+
4. **安全 0 值兜底与浮点自动取整 (`repaired`)**:浮点数值与带小数数字字符串通过 `Math.round()` 转换为有效非负整数,彻底满足 Zod `z.number().int().nonnegative()` 模式校验;
|
|
82
|
+
5. **内存投影注册表动态切入与 WeakMap 高性能缓存 (`lib/patch.js`, `lib/index.js`)**:无缝覆盖全部 10–15 个并发投影,同一事件仅处理一次,其余投影 $O(1)$ 快速返回;
|
|
83
|
+
6. **防刷屏告警与 O(1) FIFO 内存保护 (`told`)**:容量上限为 1,000 项,达到上限时平滑淘汰最老记录,杜绝内存泄漏与误抑制;
|
|
84
|
+
7. **原生 Web UI 设置卡片 (`lib/client.js`)**:嵌入 DSH 原生设置中心(`settings.plugin.item`),实时状态徽章、3 秒自动消失保存提示与多语言支持。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🚀 v0.1.3 版本更新说明 (Changed in v0.1.3)
|
|
89
|
+
|
|
90
|
+
* **浮点与小数 Token 防崩保护 (Floats & Decimals)**:
|
|
91
|
+
- DeepSeek Harness `@deepseek-ai/dsh-token-meter` 的投影模式严格要求整数 (`z.number().int().nonnegative()`)。部分路由网关返回的非整数计数(如 `42.5`)此前会导致 Zod 校验失败。
|
|
92
|
+
- `sound()` 函数在 v0.1.3 中严格要求 `Number.isInteger(value)`。
|
|
93
|
+
- 浮点数及带小数点的数字字符串通过 `Math.round()` 自动四舍五入为有效非负整数(`42.6` $\rightarrow$ `43`),确保会话历史永不白屏。
|
|
94
|
+
* **WeakMap 高性能事件缓存 (`guard`)**:
|
|
95
|
+
- 会话重放时有 10–15 个并发投影同时调用拦截器。
|
|
96
|
+
- 引入 `WeakMap<event, guardedEvent>` 微缓存:事件在首次调用时清洗并缓存,其余并发投影直接以 $O(1)$ 获取结果,消除重复克隆并杜绝内存泄漏。
|
|
97
|
+
* **日志告警缓存 FIFO 平滑淘汰 (`told`)**:
|
|
98
|
+
- 将达到 1,000 条上限时的全量清空 `told.clear()` 改为 $O(1)$ 单条最旧数据淘汰 `told.delete(oldest)`,避免重新打开历史会话时日志重复刷屏。
|
|
99
|
+
* **Web UI 设置卡片交互体验与无障碍优化**:
|
|
100
|
+
- 保存成功提示("已保存")支持 3 秒自动平滑淡出,且在修改任一复选框时立即重置。
|
|
101
|
+
- 支持在无未提交草稿时平滑同步服务端后台配置。
|
|
102
|
+
- 为表单项绑定 `id`/`htmlFor` 关联,并为折叠图标补充 `aria-hidden="true"` 无障碍属性。
|
|
85
103
|
|
|
86
104
|
---
|
|
87
105
|
|
package/lib/client.js
CHANGED
|
@@ -22,6 +22,8 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
stroke: 'currentColor',
|
|
23
23
|
strokeWidth: 1.5,
|
|
24
24
|
style: props.style,
|
|
25
|
+
className: props.className,
|
|
26
|
+
'aria-hidden': true,
|
|
25
27
|
}, React.createElement('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' }));
|
|
26
28
|
}
|
|
27
29
|
const Chevron = ChevronIcon || FallbackChevron;
|
|
@@ -34,6 +36,14 @@ window.__ModuleLoader__.load({
|
|
|
34
36
|
const [msg, setMsg] = React.useState('');
|
|
35
37
|
|
|
36
38
|
const initializedRef = React.useRef(false);
|
|
39
|
+
const isDirtyRef = React.useRef(false);
|
|
40
|
+
const msgTimerRef = React.useRef(null);
|
|
41
|
+
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
return () => {
|
|
44
|
+
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
37
47
|
|
|
38
48
|
React.useEffect(() => {
|
|
39
49
|
let off = null;
|
|
@@ -46,7 +56,7 @@ window.__ModuleLoader__.load({
|
|
|
46
56
|
const update = () => {
|
|
47
57
|
const s = scope.getSnapshot ? scope.getSnapshot() : { status: 'unavailable', value: null };
|
|
48
58
|
setSnap(s);
|
|
49
|
-
if (s.status === 'ready' && s.value && !initializedRef.current) {
|
|
59
|
+
if (s.status === 'ready' && s.value && (!initializedRef.current || !isDirtyRef.current)) {
|
|
50
60
|
initializedRef.current = true;
|
|
51
61
|
setDraft({
|
|
52
62
|
repair: s.value.repair ?? true,
|
|
@@ -72,12 +82,22 @@ window.__ModuleLoader__.load({
|
|
|
72
82
|
}
|
|
73
83
|
})();
|
|
74
84
|
|
|
85
|
+
const showMessage = (text, autoDismiss = false) => {
|
|
86
|
+
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
87
|
+
setMsg(text);
|
|
88
|
+
if (autoDismiss) {
|
|
89
|
+
msgTimerRef.current = setTimeout(() => {
|
|
90
|
+
setMsg('');
|
|
91
|
+
}, 3000);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
75
95
|
const onSave = async () => {
|
|
76
96
|
setSaving(true);
|
|
77
97
|
setMsg('');
|
|
78
98
|
try {
|
|
79
99
|
if (!ctx.settingsScope || !ctx.settingsScope.bind) {
|
|
80
|
-
|
|
100
|
+
showMessage('settingsScope unavailable', false);
|
|
81
101
|
setSaving(false);
|
|
82
102
|
return;
|
|
83
103
|
}
|
|
@@ -91,16 +111,26 @@ window.__ModuleLoader__.load({
|
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
if (errs.length) {
|
|
94
|
-
|
|
114
|
+
showMessage(errs.join('; '), false);
|
|
95
115
|
} else {
|
|
96
|
-
|
|
116
|
+
isDirtyRef.current = false;
|
|
117
|
+
showMessage(t('saved') || 'Saved', true);
|
|
97
118
|
}
|
|
98
119
|
} catch (e) {
|
|
99
|
-
|
|
120
|
+
showMessage(String(e && e.message ? e.message : e), false);
|
|
100
121
|
}
|
|
101
122
|
setSaving(false);
|
|
102
123
|
};
|
|
103
124
|
|
|
125
|
+
const handleToggle = (field, checked) => {
|
|
126
|
+
isDirtyRef.current = true;
|
|
127
|
+
if (msg) {
|
|
128
|
+
if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
|
|
129
|
+
setMsg('');
|
|
130
|
+
}
|
|
131
|
+
setDraft((prev) => ({ ...prev, [field]: checked }));
|
|
132
|
+
};
|
|
133
|
+
|
|
104
134
|
const isRepairActive = draft.repair !== false;
|
|
105
135
|
const badgeLabel = isRepairActive ? (t('active') || 'ACTIVE') : (t('reportOnly') || 'REPORT ONLY');
|
|
106
136
|
const badgeClass = isRepairActive ? 'ug-badge ug-badge-green' : 'ug-badge ug-badge-yellow';
|
|
@@ -132,32 +162,34 @@ window.__ModuleLoader__.load({
|
|
|
132
162
|
React.createElement('span', { className: 'ug-title' }, t('title') || 'Usage Guard'),
|
|
133
163
|
React.createElement('span', { className: 'ug-sub' }, t('subtitle') || 'Token-usage sanitizer & session crash protection'),
|
|
134
164
|
React.createElement('span', { className: badgeClass }, badgeLabel),
|
|
135
|
-
React.createElement(Chevron, { className: 'ug-chev' + (expanded ? ' ug-chev-open' : '') })
|
|
165
|
+
React.createElement(Chevron, { className: 'ug-chev' + (expanded ? ' ug-chev-open' : ''), 'aria-hidden': true })
|
|
136
166
|
),
|
|
137
167
|
expanded ? React.createElement('div', { className: 'ug-body' },
|
|
138
168
|
snap.status === 'loading' ? React.createElement('div', { className: 'ug-field', style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, t('loading') || 'Loading…') : null,
|
|
139
169
|
snap.status === 'unavailable' ? React.createElement('div', { className: 'ug-field', style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, t('unavailable') || 'Settings scope unavailable') : null,
|
|
140
170
|
snap.status === 'ready' ? React.createElement('div', null,
|
|
141
171
|
React.createElement('div', { className: 'ug-field' },
|
|
142
|
-
React.createElement('label', { className: 'ug-row' },
|
|
172
|
+
React.createElement('label', { className: 'ug-row', htmlFor: 'ug-repair-toggle' },
|
|
143
173
|
React.createElement('span', null, t('repair') || 'Auto-repair token counters'),
|
|
144
174
|
React.createElement('input', {
|
|
175
|
+
id: 'ug-repair-toggle',
|
|
145
176
|
type: 'checkbox',
|
|
146
177
|
className: 'ug-checkbox',
|
|
147
178
|
checked: !!draft.repair,
|
|
148
|
-
onChange: (e) =>
|
|
179
|
+
onChange: (e) => handleToggle('repair', e.target.checked),
|
|
149
180
|
})
|
|
150
181
|
),
|
|
151
182
|
React.createElement('div', { className: 'ug-sub' }, t('repairDesc') || 'Replace missing or non-numeric counters with 0 before summing to prevent NaN history lockups')
|
|
152
183
|
),
|
|
153
184
|
React.createElement('div', { className: 'ug-field' },
|
|
154
|
-
React.createElement('label', { className: 'ug-row' },
|
|
185
|
+
React.createElement('label', { className: 'ug-row', htmlFor: 'ug-report-toggle' },
|
|
155
186
|
React.createElement('span', null, t('report') || 'Log diagnostic warnings'),
|
|
156
187
|
React.createElement('input', {
|
|
188
|
+
id: 'ug-report-toggle',
|
|
157
189
|
type: 'checkbox',
|
|
158
190
|
className: 'ug-checkbox',
|
|
159
191
|
checked: !!draft.report,
|
|
160
|
-
onChange: (e) =>
|
|
192
|
+
onChange: (e) => handleToggle('report', e.target.checked),
|
|
161
193
|
})
|
|
162
194
|
),
|
|
163
195
|
React.createElement('div', { className: 'ug-sub' }, t('reportDesc') || 'Log a line naming the turn, step, and raw sample whenever damaged usage arrives')
|
package/lib/index.js
CHANGED
|
@@ -59,22 +59,39 @@ export function apply(ctx, config) {
|
|
|
59
59
|
// О каждом сочетании сессии, хода, шага и набора полей жалуемся один раз:
|
|
60
60
|
// выжимка считается проигрыванием журнала заново, и без этого одна и та же
|
|
61
61
|
// порция въезжала бы в журнал при каждом открытии истории.
|
|
62
|
-
//
|
|
62
|
+
// Честное FIFO-вытеснение с ограничением в 1000 записей предотвращает утечку
|
|
63
|
+
// памяти при длительной работе без сброса всей накопленной дедупликации.
|
|
63
64
|
const told = new Set()
|
|
64
65
|
const MAX_TOLD = 1000
|
|
65
66
|
|
|
67
|
+
// Кэш санитизированных событий: каждое событие проходит через 10-15 проекций DSH.
|
|
68
|
+
// WeakMap позволяет обработать событие один раз, отдавая остальным проекциям
|
|
69
|
+
// готовый результат за O(1) без повторного парсинга и без утечек памяти.
|
|
70
|
+
const eventCache = new WeakMap()
|
|
71
|
+
|
|
66
72
|
const guard = (event) => {
|
|
73
|
+
if (!event || typeof event !== 'object') return event
|
|
74
|
+
const cached = eventCache.get(event)
|
|
75
|
+
if (cached !== undefined) return cached
|
|
76
|
+
|
|
67
77
|
const found = usageOf(event)
|
|
68
|
-
if (!found)
|
|
78
|
+
if (!found) {
|
|
79
|
+
eventCache.set(event, event)
|
|
80
|
+
return event
|
|
81
|
+
}
|
|
69
82
|
const bad = damage(found.usage)
|
|
70
|
-
if (bad.length === 0)
|
|
83
|
+
if (bad.length === 0) {
|
|
84
|
+
eventCache.set(event, event)
|
|
85
|
+
return event
|
|
86
|
+
}
|
|
71
87
|
|
|
72
88
|
if (live.report !== false) {
|
|
73
89
|
const sessionTag = event?.sessionId ?? event?.data?.sessionId ?? event?.data?.turn ?? 'global'
|
|
74
90
|
const seen = `${String(sessionTag)}/${String(found.turn)}/${String(found.step)}/${bad.join(',')}`
|
|
75
91
|
if (!told.has(seen)) {
|
|
76
92
|
if (told.size >= MAX_TOLD) {
|
|
77
|
-
told.
|
|
93
|
+
const oldest = told.values().next().value
|
|
94
|
+
told.delete(oldest)
|
|
78
95
|
}
|
|
79
96
|
told.add(seen)
|
|
80
97
|
try {
|
|
@@ -86,7 +103,9 @@ export function apply(ctx, config) {
|
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
|
|
106
|
+
const result = live.repair === false ? event : healed(event, bad)
|
|
107
|
+
eventCache.set(event, result)
|
|
108
|
+
return result
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
ctx.inject(['sessionProjections'], (pctx) => {
|
package/lib/usage.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// Счётчики токенов: что проверяем и как чиним.
|
|
2
|
+
//
|
|
3
|
+
// Харнесс складывает расход в четыре ведра:
|
|
4
|
+
//
|
|
5
|
+
// uncachedInputTokens: usage.inputTokens,
|
|
6
|
+
// outputTokens: usage.outputTokens,
|
|
7
|
+
// cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
8
|
+
// cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
9
|
+
//
|
|
10
|
+
// Первые два он берёт без проверки. Сложение `total += undefined` даёт NaN,
|
|
11
|
+
// а проверка схемы в ядре требует неотрицательное целое число (zod.int().nonnegative()).
|
|
12
|
+
// Из-за этого одна битая порция валит не просто
|
|
13
|
+
// счётчик, а всю историю сессии.
|
|
11
14
|
export const FIELDS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -51,25 +54,28 @@ const ALIASES = {
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
|
-
* Годное значение счётчика: конечное неотрицательное число.
|
|
55
|
-
* Значения меньше нуля (-1 и т.д.)
|
|
56
|
-
* так как схема DSH требует nonnegative
|
|
57
|
+
* Годное значение счётчика: конечное неотрицательное ЦЕЛОЕ число.
|
|
58
|
+
* Значения меньше нуля (-1 и т.д.), нечисловые, а также дробные числа (floats)
|
|
59
|
+
* не признаются годными, так как схема ядра DSH требует z.number().int().nonnegative().
|
|
57
60
|
*/
|
|
58
61
|
function sound(value) {
|
|
59
|
-
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
62
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && Number.isInteger(value)
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
66
|
+
* Безопасное приведение значения к корректному неотрицательному целому числу.
|
|
67
|
+
* Округляет дробные числа и парсит числовые строки (в т.ч. с десятичной точкой).
|
|
65
68
|
*/
|
|
66
69
|
function coerceNumber(value) {
|
|
67
70
|
if (typeof value === 'number') {
|
|
68
|
-
return Number.isFinite(value) && value >= 0 ? value : undefined
|
|
71
|
+
return Number.isFinite(value) && value >= 0 ? Math.round(value) : undefined
|
|
69
72
|
}
|
|
70
|
-
if (typeof value === 'string'
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
const trimmed = value.trim()
|
|
75
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
76
|
+
const num = Number(trimmed)
|
|
77
|
+
return Number.isFinite(num) && num >= 0 ? Math.round(num) : undefined
|
|
78
|
+
}
|
|
73
79
|
}
|
|
74
80
|
return undefined
|
|
75
81
|
}
|
|
@@ -100,8 +106,8 @@ export function usageOf(event) {
|
|
|
100
106
|
*
|
|
101
107
|
* Отсутствие кэш-полей нарушением не считается: их не присылает добрая половина
|
|
102
108
|
* провайдеров, и харнесс сам подставляет там ноль. Ругаемся только на то, что
|
|
103
|
-
* действительно ломает сложение, — на пропущенные,
|
|
104
|
-
*
|
|
109
|
+
* действительно ломает сложение, — на пропущенные, нечисловые, нецелые или
|
|
110
|
+
* отрицательные значения.
|
|
105
111
|
*/
|
|
106
112
|
export function damage(usage) {
|
|
107
113
|
if (!usage || typeof usage !== 'object') return FIELDS.slice(0, 2)
|
|
@@ -116,8 +122,8 @@ export function damage(usage) {
|
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
/**
|
|
119
|
-
* Число под одним из привычных чужих имён, вложенных структур или
|
|
120
|
-
* Возвращает валидное неотрицательное число или `undefined`, если его там нет.
|
|
125
|
+
* Число под одним из привычных чужих имён, вложенных структур или числовых строк/дробей.
|
|
126
|
+
* Возвращает валидное целое неотрицательное число или `undefined`, если его там нет.
|
|
121
127
|
*/
|
|
122
128
|
export function borrowed(usage, field) {
|
|
123
129
|
if (!usage || typeof usage !== 'object') return undefined
|
|
@@ -135,7 +141,7 @@ export function borrowed(usage, field) {
|
|
|
135
141
|
if (num !== undefined) return num
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
// 3. Само поле, если оно было передано валидной строкой (напр. "123")
|
|
144
|
+
// 3. Само поле, если оно было передано валидной строкой или нецелым числом (напр. "123" или 15.4)
|
|
139
145
|
const selfNum = coerceNumber(usage[field])
|
|
140
146
|
if (selfNum !== undefined) return selfNum
|
|
141
147
|
|
|
@@ -145,11 +151,9 @@ export function borrowed(usage, field) {
|
|
|
145
151
|
/**
|
|
146
152
|
* Порция с обезвреженными счётчиками.
|
|
147
153
|
*
|
|
148
|
-
* Сначала ищем число под чужим
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* поля порции остаются как были: чиним арифметику, а не переписываем ответ
|
|
152
|
-
* провайдера.
|
|
154
|
+
* Сначала ищем число под чужим именем или нормализуем само поле (округление float/string),
|
|
155
|
+
* и только если его нет — ставим ноль.
|
|
156
|
+
* Чиним арифметику, а не переписываем остальной ответ провайдера.
|
|
153
157
|
*/
|
|
154
158
|
export function repaired(usage, bad) {
|
|
155
159
|
const fixed = usage && typeof usage === 'object' ? { ...usage } : {}
|
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.3",
|
|
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",
|