# 📦 @goodandready/dsh-usage-guard

<div align="center">

<h3>Санитайзер расхода токенов, защита от сбоев истории и сохранение сессий для DeepSeek Harness</h3>

<p align="center">
  <a href="https://www.npmjs.com/package/@goodandready/dsh-usage-guard"><img src="https://img.shields.io/npm/v/@goodandready/dsh-usage-guard.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
  <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
</p>

<p align="center">
  <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
</p>

<p align="center">
  <a href="README.md"><b>🇬🇧 English</b></a> •
  <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
</p>

<table align="center">
  <tr>
    <td align="center">
      ⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
      <br><br>
      🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
    </td>
  </tr>
</table>

</div>

---

## ⚡ В чём суть проблемы: как провайдеры ломают историю сессий

В ядре **DeepSeek Harness** подсчёт суммарного расхода токенов складывается из четырёх счётчиков:

```javascript
uncachedInputTokens: usage.inputTokens,        // Без подстраховки в ядре DSH!
outputTokens:        usage.outputTokens,       // Без подстраховки в ядре DSH!
cacheReadTokens:     usage.cacheReadTokens ?? 0,
cacheWriteTokens:    usage.cacheWriteTokens ?? 0,
```

Поля кэша ядро подстраховывает через `?? 0`, а первые два берёт **как есть, без проверки на число**.

Когда сторонний провайдер, локальный движок инференса, шлюз или роутер присылает нестандартные названия полей, пустые значения или `NaN`, стандартное сложение (`total += usage.inputTokens`) превращает сумму расхода сессии в `NaN`.

После этого валидация схемы в ядре отвергает выжимку сессии:
```
history unavailable for session "<session-id>": expected number, received NaN
```

Поскольку история сессий в DSH рассчитывается на лету **путём повторного проигрывания журнала событий**, одна-единственная битая порция токенов навсегда блокирует открытие диалога в веб-интерфейсе.

```mermaid
graph LR
    subgraph Malformed [Поток данных провайдера]
        API[Ответ модели] -->|Присылает prompt_tokens / NaN / null| Event[Событие сессии]
    end

    subgraph Unprotected [Без dsh-usage-guard]
        Event --> DSHMath[Сложение расхода в ядре DSH]
        DSHMath -->|total += NaN| Poison[🚨 Сумма расхода становится NaN]
        Poison --> SchemaFail[Отказ проверки схемы]
        SchemaFail --> DeadHistory[💥 История сессии заблокирована навсегда]
    end

    subgraph Guarded [С активным dsh-usage-guard]
        Event --> Patch[Перехватчик sessionProjections]
        Patch --> AliasCheck{Поиск по синонимам}
        AliasCheck -->|prompt_tokens -> inputTokens| Restored[Восстановленное число]
        AliasCheck -->|Если число отсутствует| ZeroFallback[Безопасный 0]
        Restored --> SafeMath[Корректное сложение]
        ZeroFallback --> SafeMath
        SafeMath --> ValidHistory[✅ 100% Восстановленная и сохранная история]
    end

    style Malformed fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
    style Unprotected fill:#311b1b,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4
    style Guarded fill:#181825,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
```

---

## ✨ Ключевые возможности и механизмы защиты

### 1. Мгновенное восстановление ранее сломанных сессий
Плагин **не модифицирует** файлы журналов на диске. Он встаёт перед операцией сложения в памяти. Так как проигрывание сессий идёт через эту же точку, **все ранее повреждённые сессии начинают открываться снова сразу после установки плагина**.

### 2. Полный словарь синонимов полей (`borrowed`)
Прежде чем подставлять ноль, плагин ищет значения в общепринятых полях других API:

| Целевое поле DSH | Распознаваемые синонимы |
|---|---|
| `inputTokens` | `input_tokens`, `input`, `promptTokens`, `prompt_tokens`, `promptTokenCount`, `prompt_eval_count` |
| `outputTokens` | `output_tokens`, `output`, `completionTokens`, `completion_tokens`, `candidatesTokenCount`, `eval_count` |
| `cacheReadTokens` | `cache_read_tokens`, `cachedTokens`, `cached_tokens`, `cache_read_input_tokens`, `cachedContentTokenCount`, `prompt_tokens_details.cached_tokens` |
| `cacheWriteTokens` | `cache_write_tokens`, `cacheCreationTokens`, `cache_creation_input_tokens` |

### 3. Проверка на конечное неотрицательное целое число (`sound`)
Проверка `typeof value === 'number' && Number.isFinite(value) && value >= 0 && Number.isInteger(value)` гарантирует отсечение `NaN`, `Infinity`, `null`, `undefined`, отрицательных чисел (напр. `-1`), нецелых дробей (floats) и строк.

### 4. Безопасная подстановка нуля и округление (`repaired`)
Если число найти не удалось, подставляется `0`. Валидные дробные токены или числовые строки округляются до целых через `Math.round()`, строго удовлетворяя Zod-контракту ядра DSH `z.number().int().nonnegative()`.

### 5. Безопасный перехват в памяти (`lib/patch.js`)
* **Существующие проекции**: оборачивает все методы `.apply` в `sessionProjections.registrations` с полным сохранением контекста `this`.
* **Поздние проекции**: перехватывает будущие регистрации через хук `map.set`.
* **Комплексная защита**: защищает не только токены, но и формулы давления на контекст и разбор занятости.
* **Нулевой оверхед**: поверхностное клонирование применяется только к объекту расхода, а `WeakMap` устраняет повторные вызовы при обходе 10–15 проекций DSH.

### 6. Дедуплицированное журналирование (`told`)
Выводит предупреждение с идентификатором сессии, номером хода, шагом и способом починки (взят по синониму или обнулён). Одинаковые инциденты не спамят в лог при повторных проигрываниях, а размер кэша ограничен 1000 записями с честным O(1) FIFO-вытеснением для предотвращения утечек памяти.

### 7. Нативная карточка в Web UI (`lib/client.js`)
* Карточка настроек встроена во вкладку «Настройки → Плагины → Настройки плагинов» (`settings.plugin.item`) с динамическим бейджем статуса, плавно исчезающим подтверждением сохранения и полной локализацией.

---

## 🚀 Изменения в версии v0.1.9 (Changed in v0.1.9)

* **Исправление валидации схемы Cordis Schemastery (`z.natural()`)**:
  - Заменена несовместимая цепочка Zod `z.number().int().nonnegative()` в схеме `Config` на нативный метод Cordis Schemastery `z.natural()`.
  - Устранена критическая ошибка `TypeError: z.number(...).int is not a function`, вызывавшая аварийную остановку лоадера Cordis при запуске `dsh-web.service`.
  - Добавлен регрессионный юнит-тест `test/config.test.mjs`.

## 🚀 Изменения в версии v0.1.8 (Changed in v0.1.8)

* **Канонизация локалей (en/zh) и делегирование русского перевода**:
  - Бандл `lib/client.js` поставляет исключительно канонический английский (`en`) и китайский (`zh`) словари.
  - Русская локализация вынесена и поддерживается централизованно через `@goodandready/dsh-russian-lang` (Issue #196).
  - Автотесты строго контролируют отсутствие кириллических строк в клиентском бандле.
* **Живая телеметрия перехватов (`/api/dsh-usage-guard/telemetry`)**:
  - Накапливает в оперативной памяти счётчики `rescuedEvents`, `fixedTokens`, `clampedSpikes` и кольцевой FIFO-буфер недавних инцидентов.
  - Добавлен блок `TelemetrySection` в карточке настроек с отображением 3 показателей и деталей последнего инцидента.
* **Встроенный One-Click Updater (`/api/dsh-usage-guard/update`)**:
  - Реализован протокол обновления плагинов DSH (проверка версий и вызов DSH CLI).
  - Защита от SSRF/CSRF через `isTrustedUpdateRequest`: проверка Loopback, Same-Origin, совпадения `host`/`origin` и токена `x-dsh-plugin-update: 1`.
* **Срезание аномальных спайков токенов (`maxStepTokens`)**:
  - Конфигурируемый порог в схеме ядра и UI для ограничения аномальных счетчиков шага (> 1 000 000).
  - Отдельное логирование и учёт в телеметрии для срезанных спайков.

## 🚀 Изменения в версии v0.1.7 (Changed in v0.1.7)

* **Единый стиль по референсу `dsh-clinebot` (#6)**:
  - Идемпотентный вызов `ensureCss()` вынесен за пределы цикла рендера (`#dsh-usage-guard-full-css` с атрибутом `data-dsh-plugin="dsh-usage-guard"`).
  - Карточка настроек защищена компонентом `ErrorBoundary`: ошибки интерфейса не роняют панель настроек DSH и сопровождаются кнопкой «Retry».
  - Полное использование токенов темы DSH для карточек, полей и бейджей (`.ug-section-card`, `.ug-field-card`, `.ug-stat-box`, `.ug-badge-ok`, `.ug-badge-warn`, `.ug-btn-primary`).
  - Добавлен блок телеметрии статуса защиты и целевой службы `sessionProjections`.
  - Внедрена функция локализации `makeT(ru, en)` с поддержкой интерполяции переменных `{var}` и fallback.
  - Фоновая синхронизация зеркала настроек `refreshMirrorUntilVisible(ctx)` с безопасным unref-таймером.
* **Аудит стабильности и расширение синонимов расхода**:
  - Добавлена поддержка вложенных структур кэша OpenAI (`prompt_tokens_details.cachedTokens`, `prompt_tokens_details.cacheCreationTokens`).
  - Усилена функция приведения чисел (`coerceNumber`) против `Infinity`, `-Infinity`, `NaN` и мусорных строк.
  - Безопасная обработка настроек ядра и перехват исключений хоста.

## 🚀 Изменения в версии v0.1.5 (Changed in v0.1.5)

* **Строгая регистрация в `settings.plugin.item` (#3)**:
  Полностью удалена устаревшая ветка регистрации в боковом меню `settings.section`. По канону DSH Plugin Authoring карточка настроек размещается исключительно во вкладке «Настройки → Плагины» (`settings.plugin.item`) с ключом `dsh-usage-guard`, не занимая ограниченный ресурс бокового списка.
* **Очистка клиентской архитектуры**:
  Регистрация интерфейса упрощена до единственного прямого вызова без задержек и fallback-таймеров.

## 🚀 Изменения в версии v0.1.4 (Changed in v0.1.4)

* **Изоляция стилей через `data-dsh-plugin`**:
  Динамический тег `<style>` помечен атрибутом `data-dsh-plugin="dsh-usage-guard"`, предотвращая сброс стилей карточки при HMR или обновлении соседних плагинов.
* **Прямая регистрация слота**:
  Заменён некорректный вызов `ctx.slots.inject()` на прямой `ctx.slots.register('settings.plugin.item', ...)`.
* **Канонический стандарт локализации**:
  Клиентский модуль регистрирует только канонический словарь `en`, передавая управление переводами системному translation-плагину.
* **Безопасная инициализация и англоязычные логи**:
  Вызов `Config()` защищён блоком `try...catch`, а системные предупреждения в консоли переведены на английский язык.

## 🚀 Изменения в версии v0.1.3 (Changed in v0.1.3)

* **Защита от дробных токенов (Floats & Decimals)**:
  - Схема валидации DSH ядра `@deepseek-ai/dsh-token-meter` требует строго целые числа (`z.number().int().nonnegative()`). Дробные токены (например, `42.5` или `"1540.2"` при усреднении или конвертации весов моделями) ранее приводили к падению схемы Zod.
  - В v0.1.3 функция `sound()` строго требует `Number.isInteger(value)`.
  - Все дробные числа и числовые строки с плавающей точкой теперь безопасно и корректно округляются через `Math.round()` (`42.6` $\rightarrow$ `43`), сохраняя читаемость истории сессий.
* **Высокопроизводительное WeakMap-кэширование (`guard`)**:
  - В DSH активно 10–15 параллельных проекций, каждая из которых вызывала перехватчик `apply()`.
  - Внедрён микрокэш на базе `WeakMap<event, guardedEvent>`. Санитизация события выполняется ровно 1 раз при первой проекции, а остальные 14 проекций получают уже нормализованное событие за $O(1)$ без лишнего клонирования и без риска утечек памяти.
* **Честное FIFO-вытеснение в кэше логов (`told`)**:
  - Заменена полная очистка `told.clear()` на удаление старейшей записи `told.delete(oldest)` за $O(1)$ при достижении лимита в 1000 записей, что предотвращает повторные лавины логов при открытии старых диалогов.
* **Улучшение UX и доступности Web UI карточки настроек**:
  - Сообщение об успешном сохранении («Сохранено») теперь плавно исчезает через 3 секунды и мгновенно сбрасывается при любом изменении переключателей.
  - Добавлена реактивная синхронизация настроек с фоновыми изменениями сервера при нередактируемом драфте.
  - Улучшена доступность: добавлены атрибуты `id` и `htmlFor` к полям формы, а иконке шеврона проставлен `aria-hidden="true"`.

---

## 🚀 Изменения в версии v0.1.2 (Changed in v0.1.2)

* **Нативная карточка настроек Web UI (`settings.plugin.item`)**:
  - Добавлен клиентский модуль `lib/client.js`, реализующий нативную карточку управления во вкладке «Настройки → Плагины → Настройки плагинов» с пространством ключа `dsh-usage-guard` (Issue #2).
  - Интерактивное управление параметрами `repair` (автоматическое исправление расхода) и `report` (вывод диагностических предупреждений в консоль).
  - Динамический бейдж в шапке карточки (`АКТИВЕН` при включённом исправлении, `ТОЛЬКО ЛОГ` при пассивном аудите) без необходимости раскрывать карточку.
  - Полная поддержка темы DSH (нативные CSS-переменные темы, шеврон ядра `IconChevronDownOutline14`, радиус скругления 12px, доступность `aria-expanded`).
  - Полноценная локализация интерфейса карточки на русский, английский и китайский языки (`en`, `ru`, `zh`).
  - Запасной fallback-слот `settings.section` на случай запуска в сборках DSH без вкладки настроек плагинов.
* **Дизайн-контракт**:
  - Создан обязательный документ `docs/design/DESIGN.md` в соответствии со стандартами `project-design-contract` и `dsh-ui-design`.

---

## 🚀 Изменения в версии v0.1.1 (Changed in v0.1.1)

* **Защита от отрицательных чисел (`nonnegative`)**:
  В v0.1.0 значение `-1` (возвращаемое некоторыми шлюзами при ошибках) проходило проверку конечности числа и ломало схему DSH (`z.number().int().nonnegative()`). Начиная с v0.1.1 функция `sound()` строго требует `value >= 0`. Отрицательные значения теперь признаются повреждёнными и безопасно обнуляются.
* **Безопасная коэрция числовых строк (Stringified Numbers)**:
  Если провайдер возвращает валидное количество токенов строкой (например, `inputTokens: "1540"`), плагин в v0.1.1 больше не сбрасывает его в ноль, а безопасно приводит к настоящему числу (`1540`).
* **Расширенная совместимость с экосистемами моделей**:
  - **Google Gemini API**: добавлены `promptTokenCount`, `candidatesTokenCount`, `cachedContentTokenCount`.
  - **Ollama native API**: добавлены `prompt_eval_count`, `eval_count`.
  - **OpenAI prompt caching**: добавлена поддержка вложенного объекта `prompt_tokens_details.cached_tokens`.
* **Сохранение контекста вызова `this` в проекциях**:
  В `lib/patch.js` метод `wrapApply` переписан с сохранением `this` (`original.call(this, state, guard(event))`), обеспечивая полную совместимость с объектно-ориентированными проекциями.
* **Отказоустойчивое логирование без риска краша**:
  В функции `complaint()` сериализация данных экранирована `try...catch`, предотвращая падение рантайма при наличии циклических структур или `BigInt`.
* **Изолированная межсессионная дедупликация**:
  Ключ дедупликации предупреждений дополнен контекстом сессии (`sessionId`), что предотвращает ложное подавление сообщений об ошибках в последующих диалогах. Множество `told` ограничено 1000 элементами для защиты от утечек памяти.

---

## 📦 Быстрая установка

```bash
dsh plugin --profile web add @goodandready/dsh-usage-guard
```

---

## ⚙️ Параметры конфигурации (`settings.yaml` / Web UI)

```yaml
dsh-usage-guard:
  repair: true
  report: true
```

| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| `repair` | `boolean` | `true` | Заменять пропущенные и нечисловые значения на ноль до сложения |
| `report` | `boolean` | `true` | Логировать информацию о поврежденных порциях в консоль |

---

## 📄 Лицензия

MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)