npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-session-guard

v4.1.16

Published

高峰自动会话门:周末模式 + 高峰自动暂停 + 官方源二维判定 + 会话级冻结 + 后端自动重试。

Readme

dsh-session-guard

Замечание о совместимости: в v0.1.1 уже входят словари японского (ja) и корейского (ko), но текущие официальные сборки DSH предоставляют через LocaleRuntime только zh и en. В оригинальном DSH выбор ja или ko завершается ошибкой locale "<id>" is not registered. Придётся дождаться, пока официальный DSH добавит соответствующие идентификаторы локалей. Опытные пользователи могут поддерживать форк DSH, чтобы расширить его.

▼ Совместимость версий DSH

| Версия DSH | Загрузка | Регистрация настроек | События сессии / ворота | Клиентская половина | | --- | --- | --- | --- | --- | | 0.1.0-rc.7 ~ 0.1.1-rc.x | ➖ вне этой линии (исторические версии до legacy/0.1.2) | ctx.settings.register(ns, schema, { base }) | ✅ форма совпадает | ✅ без импорта платформенных значений | | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ➖ вне этой линии → используйте legacy/0.1.2 (npm @dsh-0.1.2) | register сохранён (добавлен installSection) | ✅ форма совпадает | ✅ без импорта платформенных значений | | 0.2.0-rc.1+ | ✅ (эта линия, dist-tag dsh-0.2.0, npm 4.0.0) | декларативно: поля .volatile() в Config хост проецирует в формы, register удалён | ✅ события читаются по двойному пути snapshotEvents() | ✅ карточка настроек клиента переведена на configForms | | 0.1.7-rc.1+ | ✅ (ветка compat/0.1.7, dist-tag dsh-0.1.7) | декларативно: поля .volatile() в Config хост проецирует в формы, register удалён | ✅ события читаются по двойному пути snapshotEvents() | ✅ карточка настроек клиента переведена на configForms | | 0.1.5-rc.2 | ✅ (ветка compat/0.1.5, dist-tag dsh-0.1.5) | register на месте (строковые пространства имён) | ✅ события читаются по двойному пути snapshotEvents() | ✅ |

Идентификация этой линии: ветка compat/0.2.0, номер версии npm 4.0.0 (semver), dist-tag dsh-0.2.0. engines.dsh в package.json и dsh.plugin.json, а также четыре peer-зависимости @deepseek-ai/dsh-client-* единообразно приведены к >=0.2.0-rc.1 <0.2.1-0; нижняя граница peer-зависимости @deepseek-ai/cordis выровнена по ^4.0.4 хостовой линии. Раньше README использовало «2.x / 3.x» как кодовые имена линий — это была манера изложения, а не номера версий, которые можно получить из реестра, — ориентируйтесь только на npm-версии 0.3.1 (линия 0.1.2), 3.0.0/3.0.1 (линия 0.1.5), 3.2.4 (линия 0.1.7) и 4.0.0 (линия 0.2.0).

Адаптация 0.2.0 (ветка compat/0.2.0, npm 4.0.0): API плагинов, которую использует этот плагин (manifest/settings/HMR/slot/сессии V4), в 0.2.0-rc.1 полностью совместима с 0.1.7; адаптация сводится к смене поколения метаданных (peer/engines/dist-tag/версия 4.0.0). Попутно с базой перенесены инженерные исправления: ① src/client/family-section.tsx добавлен под контроль версий (раньше существовал только в локальном рабочем дереве, чистый checkout не мог собрать проект); ② из конфигурации eslint убрано ядрёное no-unused-vars, ошибочно помечавшее параметры в сигнатурах типов TS, — lint-базовая линия снова полностью зелёная; ③ статические шлюзы подписей путей записи v4 (tests/source-kind.test.mjs) переносятся в эту линию вместе с базой; ④ peer-зависимость @deepseek-ai/cordis выровнена по ^4.0.4 (пакеты UI хоста 0.2.0-rc.1 объявляют ~4.0.4; прежний ^4.0.1 не конфликтовал, но его нижняя граница была слишком стара).

Где оказываются другие линии: для хостов DSH 0.1.2-rc.x используйте ветку legacy/0.1.2 (npm dist-tag dsh-0.1.2, версия 0.3.1). main заморожен на 0.2.0-beta.1 и не является веткой релизов линии 0.1.2. Для хостов DSH 0.1.0-rc.7 ~ 0.1.1-rc.x используйте исторические версии ≤ 0.1.2. Канонические определения источников полей: mine-dsh-plugins/improve-dsh-plugins/DSH-PLUGIN-VERSION-DISTRIBUTION-STRATEGY.md §2.2.

Адаптация 0.1.5 (ветка compat/0.1.5, npm 3.0.0): ① двойной путь для agent.followup — 0.1.5 превращает Inbox в проекцию agent-loop только для чтения; если followup больше не существует, происходит откат к agent.send, а если недоступно ни то ни другое — понижение до warn без исключений (страховка возобновления); ② возобновляющие сообщения добавляют в source поле form: 'instructions' (контракт ContextFormed из 0.1.5, старые версии его игнорируют); ③ webServer.register обёрнут в try/catch: неудачная регистрация лишь пишется в журнал и не рушит загрузку плагинов хоста; ④ 0.1.5 удаляет доступ к массиву session.events; чтение событий идёт по основному пути snapshotEvents() + с откатом к старому массиву (затрагивает только два вспомогательных пути findToolOutcome / lastUserPrompt, сопоставление типов событий не меняется). Проверено, что контракт WebRoute 0.1.5 (exact/prefix + SSE) не изменился: клиентскому fetch('/session-guard/...') префикс /api не нужен. Приведённая таблица совместимости охватывает два поколения API (0.1.1 / 0.1.2); эта линия претендует только на строку 0.1.5. session/event, agent.cancel, goals.pause, agent.followup, commands.register, timer.interval, webServer.register, agent/request, llm.listConfigurableProviders, settings.register/get имеют одинаковые подписи между dsh-v0.1.1-rc.2 и dsh-v0.1.2-rc.1 (эта линия проверена вплоть до dsh-v0.1.5-rc.2); единственное место, требующее двойного чтения, — форма id вызова, записываемого tool/result (приоритет у content[].toolCallId, откат к source.callId), вынесена в src/tool-call-id.js с модульными тестами — в журналах воспроизведения обеих версий могут встречаться обе формы. С 0.1.5 доступ к массиву session.events удалён; эта линия читает через snapshotEvents() (с сохранением отката к старому массиву), затрагивая только два вспомогательных пути findToolOutcome / lastUserPrompt. Событие model/selection есть только с 0.1.2, служит лишь для ускорения смены модели и обязательно требует проверки доступности функции; поверхность настроек использует только пересечение register + get (никогда installSection / удалённый installSettingsSection). Скрипт-страж дрейфа: tools/check-api-drift.ps1 (эта линия по умолчанию проверяет наличие нужных интерфейсов относительно dsh-v0.1.5-rc.2).

Автоматически приостанавливает запущенные сессии в часы пик и автоматически возобновляет их вне пиковых часов и в выходные; вместе с кнопкой заморозки input-traffic даёт блокировку на уровне сессии; автоматический повтор на стороне бэкенда уступает дорогу во время заморозки/действия ворот. В основе — собственные сессионные ворота (agent.cancel keepInbox + goals.pause + session/event безопасная граница + возобновление через followup), зависимость от dsh-task-control устранена.

Плагин cordis, собираемый командой dsh plugin и патчем бандла — без правки исходников dsh и без PR.

💡 Почему стоит установить: с 2026-08-17 DeepSeek перешёл на тарификацию по пиковой и непиковой зоне — в пиковые часы (пекинское время 9:00-12:00, 14:00-18:00) цена единицы в 2 раза выше, чем в непиковые (включая полдень, ночь, выходные и праздники). Плагин автоматически останавливает запущенные сессии в пик и автоматически возобновляет их после пика: длинные прогоны со сдвигом по времени экономят до 50 %; ручная заморозка (через кнопку input-traffic) даёт ещё более точное управление остановкой по каждой сессии.

Обзор возможностей

  • Режим выходных: распознаёт выходные (через Intl.DateTimeFormat с настроенным часовым поясом, без классической ошибки в 8 часов на границе UTC+8 из-за голого getUTCDay() для пекинского времени) → в выходные пиковая зона игнорируется, можно работать без ограничений.
  • Автопауза в пик (глобально): при входе в пик (и если не выходные) автоматически приостанавливаются все корневые сессии в состоянии running; после пика все автоматически возобновляются — глобальный переключатель, вручную ничего делать не нужно.
  • Двухмерное определение официальных источников (providerGuard): в пиковые часы блокируется только если целью запроса является официальный источник DeepSeek; с локальными/сторонними провайдерами (например local-35b) работа идёт как обычно, пиковые ворота её не трогают. Порядок определения = явный список id → эндпоинт baseURL → эндпоинт по умолчанию из каталога → встроенный id.
  • Страховка на уровне запроса + отложенная очередь: сессии, запущенные уже после входа в пик, или переключённые на ходу на официальный источник, перехватываются стражем уровня запроса agent/request (по умолчанию hold: запрос приостанавливается без ошибки и автоматически выпускается после пика).
  • Заморозка / возобновление на уровне сессии: избыточный порт sessionGuard + POST /session-guard/rpc, подключается по каждой сессии через кнопку заморозки input-traffic; есть и ручные команды /pause /resume /cancel.
  • Автоматический повтор на стороне бэкенда (D9): переходящие сбои turn/end (error/429/max-tokens) автоматически возобновляются с адаптивной задержкой; постоянные сбои останавливают работу; уступает дорогу во время заморозки/действия ворот, никогда не обходит сессионные ворота.
  • fail-open: собственные сессионные ворота недоступны, session-guard не установлен, сервис настроек отсутствует — во всех случаях тихая деградация, никогда не падает из-за зависимости.

Просмотр интерфейса

Скриншот реальной работы (Windows, dsh web) — активный режим выходных:

Установка

# Хосты DSH 0.1.5-rc.x (эта линия, dist-tag dsh-0.1.5)
dsh plugin --profile web add [email protected]

# или напрямую из ветки git
dsh plugin --profile web add github:drscrewdriver/dsh-session-guard#compat/0.1.5

# Хосты DSH 0.1.2-rc.x должны использовать линию 0.1.2
dsh plugin --profile web add [email protected]

compat/0.1.5 — выделенная линия для DSH 0.1.5-rc.x, номер версии npm 3.0.0; для хостов DSH 0.1.2-rc.x используйте ветку legacy/0.1.2 (npm dist-tag dsh-0.1.2, версия 0.3.1). main заморожен на 0.2.0-beta.1, это не ветка релизов линии 0.1.2.

⚠️ Не полагайтесь на «голое» имя пакета dsh-session-guard: тег latest в npm не может обслуживать сразу две взаимоисключающие линии версий (их диапазоны engines.dsh взаимоисключающи по правилам semver для prerelease) — dist-tag нужно указывать явно.

После установки перезапустите dsh web и обновите страницу.

Настройки (Настройки → Плагины → session-guard, простые переключатели)

| Переключатель | По умолчанию | Описание | |---|---|---| | enabled | on | Автопауза/заморозка сессий в пик: автоматически приостанавливает запущенные сессии в пиковые часы | | stepLevelPause | on | Ворота на уровне шага: в пик ворота закрываются до запроса к модели следующего шага (раньше и экономнее, чем пауза на уровне раунда); off — откат к паузе на уровне раунда | | providerGuard | on | Двухмерное определение официальных источников: в пик блокируются только официальные источники DeepSeek, локальные/сторонние провайдеры работают как обычно | | guardSubagents | on | Включая запросы субагентов: запросы субагентов тоже тарифицируются, по умолчанию блокируются вместе с остальными | | offPeakAutoResume | on | Автовозобновление в непик: приостановленные сессии автоматически возобновляются в непиковые часы; off — после пика автоматического возобновления нет (нужна ручная команда) | | weekendMode | on | Режим выходных: распознаёт выходные → в выходные автопаузы нет (в выходные нет пикового времени, можно работать свободно) | | deferredResume | on | Автовозобновление после пика: off — отложенные запросы/сессии не возобновляются сами, нужен ручной /resume | | queueFallback | on | Откат к очереди ожидания под блокировкой, когда собственные сессионные ворота недоступны (fail-open) | | retryEnabled | off | Автоповтор (бэкенд): автоматическое возобновление после переходящих сбоев (по умолчанию off, консервативно) |

Дополнительные параметры:

  • timezone (по умолчанию Asia/Shanghai) — часовой пояс для определения выходных и отображения бейджа; на определение пика не влияет (пик всегда по пекинскому времени);
  • peakWindows (по умолчанию 09:00–12:00 / 14:00–18:00) — окна пика/непика по пекинскому времени (UTC+8), совпадают с официальной тарификацией DeepSeek;
  • pauseMode (safe/force), pauseReason (wait/stop) — способ выполнения паузы;
  • stepGateTimeoutMs (по умолчанию 300000) — таймаут удержания ворот шага; по истечении ворота открываются с повышением до паузы на уровне раунда (защита от дедлока, без «одного шага каждые 5 минут» и утечки токенов);
  • Определение официальных источников: officialProviders (дополнительные id официальных провайдеров, через запятую, наивысший приоритет), officialBaseURLs (список хостов официальных эндпоинтов, по умолчанию api.deepseek.com);
  • Отложенная очередь: deferredMode (hold удержание / error ошибка и отложение), deferredResumeText (текст возобновления после пика), deferredMaxHoldMs (предел удержания, по умолчанию 6 часов, по истечении переводится в error);
  • Параметры повтора: retryText, retryGraceMs, retryCooldownMs, retryBackoffFactor, retryBackoffMaxMs, retryMaxConsecutive.

Поведение

Автоматические пиковые ворота (глобально)

  • Вход в пик (и не выходные): при включённом stepLevelPause текущий раунд больше не обрывается сразу — сессия естественно доходит до следующей границы agent/pre-step, где закрываются ворота шага (см. следующий раздел); при выключенном для всех корневых сессий running вызывается gate.stopNextTurn (настоящая пауза через собственные сессионные ворота, либо в зависимости от queueFallback откат к очереди ожидания под блокировкой);
  • Выход из пика / выходные: сначала releaseAll освобождает удерживаемые шаги (раунд продолжается на месте), затем gate.resume для всех сессий — управляется переключателем offPeakAutoResume, при выключенном после пика автоматического возобновления нет;
  • Часовой пояс пика: всегда пекинское время (Asia/Shanghai), совпадает с базой официальной тарификации DeepSeek и не зависит от настройки timezone;
  • Машина состояний: единственный экземпляр NORMAL ↔ PAUSED_PEAK (scheduler.js), управляется одним тиком в 30 с.

Ворота на уровне шага (v0.2.0, ключ к экономии токенов)

Подвешены на waterfall agent/pre-step: раунд приостанавливается до того, как произойдёт запрос к модели следующего шага.

  • Условия закрытия (все обязательны): enabled + stepLevelPause + step > 1 + пик (пекинское время, не выходные) + официальный целевой провайдер (providerGuard, при выключенном блокируются все) + сессия не удержана на уровне запроса + не обойдена вручную в этом пиковом окне;
  • Почему step > 1: первый шаг раунда покрывается стражем уровня запроса, чтобы два gate не пересекались;
  • Пути освобождения: ① кнопка «⏸ пауза (продолжить)» / POST /session-guard/rpc {action:'stepResume'} / /resume → пропускает текущий шаг и больше не перекрывает эту сессию до конца пика; ② выход из пика → всё освобождается, раунд продолжается на месте (followup не нужен); ③ кнопка заморозки / /pause / /cancel → ворота открываются и происходит переход к паузе на уровне раунда; ④ abort по signal (отмена пользователем) → открытие;
  • Повышение по таймауту: удержание дольше stepGateTimeoutMs (по умолчанию 5 минут) → ворота открываются с повышением до принудительной паузы на уровне раунда, единообразное возобновление после пика (без дедлока и без утечки токенов по капле во время пика);
  • Состояние: GET /session-guard/state?session=<id> возвращает paused: { step, turn } и stepGate: { held, since, bypass }; поле paused порта сервиса state() остаётся булевым (для обратной совместимости), состояние шага живёт в pausedStep;
  • Без записи на диск: удержание — это Promise внутри процесса, при перезапуске исчезает (чтобы не оставались «призрачные» состояния).

Кнопки «Пауза сессии / Продолжить сессию» (предоставляет session-guard)

Кнопка «Пауза сессии» справа в области ввода (slot conversation.input.right, id session-guard-pause, order 20, слева от «❄ Заморозка и добавить» у input-traffic):

  • не на паузе → «Пауза сессии», нажимается: клик вызывает stepPause и приостанавливает сессию до запроса к модели следующего шага (текущий шаг не обрывается; шаг 1 тоже перехватывается, независимо от пика и провайдера);
  • на паузе → «Продолжить сессию», клик вызывает stepResume: пропускает текущий шаг и больше не перекрывает эту сессию до конца пика;
  • Отправка событий: GET /session-guard/events?session=<id> (SSE) мгновенно доставляет изменения состояния ворот шага — как только пик автоматически закрывает ворота, кнопка тут же становится «Продолжить сессию», без ожидания опроса; вдобавок каждые 10 секунд опрашивается /session-guard/state как страховка (сходимость сохраняется и при недоступном/разорванном SSE);
  • стиль выровнен с кнопками input-traffic в той же строке (высота 24 px / радиус 6 px / шрифт 12 px / те же CSS-токены), с визуальным откликом при наведении и в состоянии паузы.

Блокировка сессии (заморозка)

  • Избыточный порт: ctx.provide('sessionGuard', service) — stopNextTurn(sessionId) / resume(sessionId) / lockQueue(sessionId) / unlockQueue(sessionId) / state(sessionId);
  • RPC-мост: POST /session-guard/rpc { action, sessionId } — кнопка заморозки input-traffic вызывает stopNextTurn / resume по каждой сессии согласно sessionId; молча пропускается, если этот плагин не установлен (fail-open D8);
  • Ручные команды: /pause [force|safe] [stop|wait], /resume [confirm] [rerun|skip], /cancel — действуют на сессию, из которой вызваны (берётся invocation.agent.id).

Автоматический повтор на стороне бэкенда (D9)

Слушает turn/end и классифицирует сбои:

  • Переходящие сбои (error/429/max-tokens и т. п.) → автоматическое возобновление followup(retryText) с адаптивной задержкой;
  • Постоянные сбои (аутентификация/баланс/модель/переполнение контекста) → остановка;
  • Уступает дорогу при заморозке/воротах: повтор не выполняется, если isFrozen(sessionId) истинно (queueLocked / paused / taskControl paused);
  • Вмешательство пользователя или успешный раунд сбрасывают счётчик последовательных сбоев.

Бейдж состояния (отображение на фронтенде)

Справа в области ввода показывается чисто информационный бейдж состояния, отражающий текущую фазу в реальном времени:

| Фаза | Текст бейджа | CSS-класс | Значение | |---|---|---|---| | peak (двухмерное определение включено) | 高峰·拦官方 | sg-peak | Пиковые часы, блокируются только запросы к официальным источникам DeepSeek | | peak (двухмерное определение выключено) | 高峰·全部暂停 | sg-peak | Пиковые часы, все сессии на паузе | | off-peak | 谷时 | sg-off | Внепиковые часы, сессии работают нормально | | weekend | 周末 | sg-weekend | Выходные (при включённом режиме выходных), пиковая зона игнорируется |

  • Опрос: каждые 15 секунд запрашивается GET /session-guard/status, чтобы получить глобальные phase, providerGuard, held, deferred, stepHeld;
  • fail-open: маршрут недоступен, сетевая ошибка или enabled выключен → бейдж тихо скрывается, ни одна сессия не затрагивается;
  • Независимость от input-traffic: бейдж рендерится только клиентом session-guard и отображается без установки плагина input-traffic. input-traffic отвечает лишь за кнопку заморозки, зависимости с бейджем нет;
  • Tooltip: при наведении показывает фаза · часовой пояс · режим выходных · база определения · числа удержаний/отложений/удержаний шага.

База определения официальных источников (providerGuard)

В пик сессии не останавливаются без разбора: сначала выясняется, «является ли маршрут, куда этот запрос реально пойдёт, официальным источником DeepSeek»:

| Приоритет | Основание | matchedBy | Пример | |---|---|---|---| | 1 | явный список id officialProviders | explicit | пользователь объявил собственный шлюз официальным | | 2 | хост нормализованной в реальном времени baseURL | endpoint | deepseek-official переключён на релей → не блокируется | | 3 | встроенный в каталог эндпоинт по умолчанию | endpoint-default | маршрут deepseek у pi-ai по умолчанию бьёт в официальный API → блокируется | | 4 | встроенный список id (deepseek-official) | route-id | откат, когда эндпоинт прочитать нельзя | | 5 | всё остальное | unknown | неофициальный, пропускается |

  • Эндпоинт важнее id: конфигурация с именем deepseek-official, но с baseURL на релей не блокируется по ошибке; наоборот, встроенный у pi-ai маршрут deepseek с эндпоинтом по умолчанию в официальный API не ускользает от блокировки.
  • Источник эндпоинтов: ctx.get('llm').listConfigurableProviders() находит запись каталога → ctx.settings.get(settingsNs) читает baseURL по settingsPath (читаются только несекретные поля, значение apiKeyEnv никогда не читается). Вычисляется заново на каждый запрос, без кэша → горячие правки конфигурации провайдера вступают в силу немедленно.
  • Переключение на неофициальный источник возвращает сессию: если после пиковой паузы сессию переключить на локального/стороннего провайдера (событие model/selection с 0.1.2+) → эта сессия возобновляется автоматически (под условием deferredResume); затрагиваются только сессии, приостановленные этим плагином при входе в пик, сессии, приостановленные вручную через /pause, никогда не трогаются. Без этого события на 0.1.1 → деградация до «следующий запрос или ручной /resume».
  • Эндпоинт недоступен для чтения: сервис llm отсутствует, структура пространства имён изменилась, поле не строка — всегда деградация к определению по id / встроенному эндпоинту с записью matchedBy, исключения никогда не выбрасываются.
  • Разбор ошибочного определения: GET /session-guard/provider?provider=<id> возвращает { official, matchedBy, endpoint }.

Страж уровня запроса и отложенная очередь

  • Зачем уровень запроса: тик в 30 с обрабатывает при скачке NORMAL → PAUSED_PEAK только те сессии, что были running в тот момент; сессии, запущенные уже после входа в пик, или переключённые на ходу на официальный источник, выпали бы. Waterfall agent/request — страховка, которая проходит с каждым запросом.
  • Определение опирается на возвращаемое значение next(): middleware выбора модели внутри waterfall перезаписывает provider/model выбранными в интерфейсе значениями, поэтому сначала нужно await next() и только потом выносить вердикт.
  • Режим hold (по умолчанию): запрос удерживается, не отправляясь и не вызывая ошибки, и выпускается в точный момент выхода из пика (msUntilOffPeak — точный таймер, тик в 30 с как страховка); отмена пользователем (abort) прерывает штатно.
  • Режим error: выбрасывается узнаваемая ошибка PEAK_DEFERRED + запись в отложенную очередь, после пика возобновление с deferredResumeText (без автоматического продолжения, если deferredResume выключен).
  • Защита потолком: если deferredMaxHoldMs (по умолчанию 6 ч) истёк, а пик не кончился → перевод в error, чтобы не зависать бесконечно.
  • Железное правило взаимного исключения: во время удержания никогда не запрашивается дополнительно пауза через сессионные ворота (пауза ждёт безопасную границу, до которой удержанный запрос никогда не дойдёт → они ожидали бы друг друга). При входе в пик уже удержанные сессии пропускаются.
  • Без персистентности: отложенная очередь — promise внутри процесса, при перезапуске исчезает.

Границы (явно вне рамок)

  • Никакой смены провайдера / перенаправления: только блокировка, без маршрутизации;
  • Компактация не проходит через agent/request: пока сессия на паузе, она происходить не может; вызванная вручную компрессия во время пика всё ещё может попасть в официальный источник (этот плагин не перехватывает уровень ctx.llm.stream);
  • На 0.1.1 нет события model/selection: автоматическое возобновление после переключения на неофициальный источник деградирует до «ждать следующий запрос или ручной /resume» (с 0.1.2 — немедленно);
  • Никаких новых npm-зависимостей, никаких чтения/записи учётных данных, повторы 429 / транспортного уровня у dsh-llm-retry не трогаются.

Работа с часовыми поясами и валидация

  • Определение часового пояса основано на именах поясов IANA (например Asia/Shanghai, Asia/Tokyo, Asia/Seoul), через Intl.DateTimeFormat проецируемых на настенные часы настроенного пояса, без опоры на голый getUTCDay() — это позволяет избежать классической ошибки в 8 часов на границе UTC+8 пекинского времени (суббота 00:30 по Пекину — в UTC ещё пятница);
  • Intl.DateTimeFormat сам служит слоем валидации: недопустимое имя пояса (например Foo/Bar) выбрасывает RangeError, которую внешний try-catch тихо понижает до пояса по умолчанию Asia/Shanghai (fail-open);
  • Окна пика левая граница включена, правая исключена [start, end), поддерживаются окна через полночь (например 22:00–06:00);
  • Настройка timezone работает одинаково для всех языков интерфейса (zh/en/ja/ko) — имена поясов IANA в Intl.DateTimeFormat не зависят от локали: при японском/корейском интерфейсе поведение часового пояса полностью совпадает с китайским.

Разделение труда с input-traffic: один «останавливает», другой «выстраивает очередь»

Оба действуют на разные звенья одной цепи, границу задаёт сама модель inbox в DSH:

Ввод пользователя ──(input-traffic выбирает ярус)──▶ очереди ожидания next-step / next-turn
                                        │
                          agent/pre-step ──(ворота шага этого плагина)──▶ пропустить / удержать
                                        │
                            agent/request ──(удержание уровня запроса этого плагина)──▶ пропустить / удержать
                                        │
                                     вызов модели

Семантика очередей DSH (две очереди, не путать)

| Очередь | Смысл | Когда потребляется | |---|---|---| | next-step | «Ввод, ожидающий следующей границы шага» | На следующем agent/pre-step: на одном уровне с результатом инструмента, ещё один шаг внутри того же раунда | | next-turn | «Промпт, ожидающий отдельного раунда» | По завершении текущего раунда, запускается как новый раунд |

Inbox.claim() всегда сначала выгребает next-step и лишь дополнительно берёт 1 элемент next-turn, если на этой границе открывается новый раунд; первый шаг раунда читает next-turn, все последующие читают next-step.

Разделение обязанностей

  • session-guard = останавливать: решает только, «когда можно продолжать», и никогда не трогает содержимое и порядок очередей.
    • ворота шага (agent/pre-step): удерживают до запроса к модели следующего шага;
    • пауза на уровне раунда (agent.cancel({keepInbox:true}) + goals.pause + безопасная граница): останавливает текущий раунд, очереди остаются как есть;
    • страж уровня запроса (удержание на agent/request): удерживает именно этот один запрос к модели.
  • input-traffic = выстраивать очередь: решает только, «в какую очередь попадает ввод пользователя, каким ярусом и когда будет потреблён».
    • три яруса = в какую очередь положить: красный «прервать» делает сначала cancel(), затем steer; жёлтый «вставить реплику» делает steer (→ next-step, следующий шаг того же раунда); зелёный «в очередь» остаётся в next-turn;
    • заморозка = извлечь строки queued + steering (ярус сохраняется) + блокировка composer + вызов sessionGuard.stopNextTurn; возобновление = снять блокировку → сначала sessionGuard.resume → повторная подача по ярусу.

Два железных правила в точке встречи

  1. Заморозка обязана позволить плагину сначала открыть ворота шага: ворота шага подвешены на agent/pre-step, а пауза на уровне раунда ждёт события на безопасной границе — они ожидали бы друг друга (pauseTask / cancelTask этого плагина сначала делают release);
  2. Пока ворота шага закрыты, сообщения уже забраны: preStep() делает inbox.claim() до диспетчеризации waterfall, поэтому новый ввод встаёт в очередь за уже забранной партией; keepInbox действует только на паузу уровня раунда.

Никаких взаимных вторжений: input-traffic не слушает agent/pre-step / agent/request (единственное исключение — явный cancel() яруса «прервать», запрошенный самим пользователем); этот плагин никогда не переписывает содержимое и порядок next-step / next-turn.

Кнопки: кнопки «Пауза сессии / Продолжить сессию» этого плагина (order 20) и кнопки input-traffic «❄ Заморозка и добавить / Возобновить и добавить» (order 30) стоят рядом и не заменяют друг друга — первые управляют воротами шага, вторые извлечением из очереди + заморозкой на уровне раунда.

Избыточный порт sessionGuard

{
  stopNextTurn(sessionId, opts),  // останавливает следующий раунд сессии (собственные сессионные ворота / откат к очереди с блокировкой)
  resume(sessionId, opts),        // возобновляет (confirm + choice: rerun|skip)
  lockQueue(sessionId, reason),   // явно блокирует очередь
  unlockQueue(sessionId),         // явно разблокирует
  stepPause(sessionId),           // вручную запрашивает паузу уровня шага (закрытие на следующей границе pre-step, шаг 1 тоже перехватывается)
  stepResume(sessionId, opts),    // открывает ворота шага (v0.2.0); при opts.bypass=false без обхода на этот пик
  state(sessionId),               // { queueLocked, lockReason, paused, pausedStep, stepHeldSince, stepBypass, taskControlAvailable, taskControl }
}

HTTP-маршруты

  • GET /session-guard/state?session=<id> — состояние сессии (включая paused: { step, turn, manual } / stepGate / последнюю цель / удержание / отложение)
  • GET /session-guard/events?session=<id> — SSE: мгновенная доставка изменений состояния ворот шага (кнопка обновляется по ним)
  • GET /session-guard/settings — настройки + доступность taskControl
  • GET /session-guard/status — текущая глобальная фаза (опрос бейджа состояния; включает stepHeld)
  • GET /session-guard/provider?provider=<id> — диагностика определения официального источника (official / matchedBy / endpoint)
  • GET /session-guard/diag — диагностика времени выполнения (включая stepGate)
  • POST /session-guard/rpc — { action: stopNextTurn|resume|lockQueue|unlockQueue|stepPause|stepResume|state, sessionId }

Хранение состояния

JSON на сессию: $DSH_HOME/.dsh/session-guard/<sessionId>.json (атомарная запись; можно переопределить через DSH_SESSION_GUARD_STATE_DIR).

Тесты

npm test   # node --test tests/*.test.mjs (часовые пояса/выходные/машина состояний/сессионные ворота/мост/повтор)

Модули

| Файл | Ответственность | |---|---| | src/time.js | Определение пика/выходных (с правильным поясом) + msUntilOffPeak (точный таймер выхода из пика) | | src/scheduler.js | Чистая машина состояний NORMAL ↔ PAUSED_PEAK | | src/provider.js | Пятиуровневое определение официальных источников (чистая функция: нормализация эндпоинта + матрица решений) | | src/provider-directory.js | Каталог эндпоинтов (llm.listConfigurableProviders + settings.get, деградация по всей цепочке) | | src/deferrals.js | Реестр отложений (удержание / освобождение / превышение предела / PeakDeferredError) | | src/request-guard.js | Страж уровня запроса на agent/request (два режима hold / error) | | src/step-gate.js | Ворота уровня шага на agent/pre-step (v0.2.0: закрытие / открытие / повышение по таймауту / bypass, чистый decideStepHold покрыт юнит-тестами) | | src/targets.js | Отслеживание «последней реальной цели» сессии (request/header + model/selection) | | src/wiring.js | Оркестрация подключения (фильтр входа в пик / подключение ворот шага / освобождение после пика / точный таймер / очистка при выгрузке) | | src/pause-gate.js | Движок собственных сессионных ворот (agent.cancel keepInbox + goals.pause + безопасная граница + возобновление followup; перед паузой сначала открывает ворота шага) | | src/pause-store.js | Персистентность собственного состояния паузы | | src/gate.js | Драйвер сессионных ворот (собственная настоящая пауза / откат к очереди с блокировкой, fail-open) | | src/bridge.js | Избыточный порт sessionGuard | | src/retry.js | Автоповтор на бэкенде (классификация сбоев/задержка/уступание при заморозке; короткое замыкание только по точному коду PEAK_DEFERRED) | | src/detect.js | Автоопределение (taskControl хоста / мост клиента input-traffic) | | src/store.js | Персистентное состояние на сессию | | src/settings.js | Подраздел настроек (схема schemastery + fail-open регистрация) | | src/index.js | apply на стороне хоста (настройки/маршруты/тик/предоставление сервиса/подключение повтора/страж запросов) | | src/client/ | Браузерная половина (кнопка паузы сессии + бейдж состояния + карточка настроек) |

Лицензия

MIT — см. LICENSE.