dsh-session-guard
v4.1.16
Published
高峰自动会话门:周末模式 + 高峰自动暂停 + 官方源二维判定 + 会话级冻结 + 后端自动重试。
Maintainers
Readme
dsh-session-guard
- English README
- 中文 README
- 日本語 README
- 한국어 README
- README en français
- README auf Deutsch
- README in italiano
- README на русском
- README en español
- Installation guide
- 中文安装指南
- 日本語インストールガイド
- 한국어 설치 안내
- Guide d'installation en français
- Installationsanleitung auf Deutsch
- Guida all'installazione in italiano
- Руководство по установке на русском
- Guía de instalación en español
- Changelog
- 日本語 changelog
- 한국어 changelog
- Changelog en français
- Changelog auf Deutsch
- Changelog in italiano
- Changelog на русском
- Changelog en español
Замечание о совместимости: в 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-tagdsh-0.2.0, npm4.0.0) | декларативно: поля.volatile()в Config хост проецирует в формы,registerудалён | ✅ события читаются по двойному путиsnapshotEvents()| ✅ карточка настроек клиента переведена наconfigForms| | 0.1.7-rc.1+ | ✅ (веткаcompat/0.1.7, dist-tagdsh-0.1.7) | декларативно: поля.volatile()в Config хост проецирует в формы,registerудалён | ✅ события читаются по двойному путиsnapshotEvents()| ✅ карточка настроек клиента переведена наconfigForms| | 0.1.5-rc.2 | ✅ (веткаcompat/0.1.5, dist-tagdsh-0.1.5) |registerна месте (строковые пространства имён) | ✅ события читаются по двойному путиsnapshotEvents()| ✅ |Идентификация этой линии: ветка
compat/0.2.0, номер версии npm4.0.0(semver), dist-tagdsh-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-tagdsh-0.1.2, версия0.3.1).mainзаморожен на0.2.0-beta.1и не является веткой релизов линии 0.1.2. Для хостов DSH0.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, сопоставление типов событий не меняется). Проверено, что контрактWebRoute0.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в тот момент; сессии, запущенные уже после входа в пик, или переключённые на ходу на официальный источник, выпали бы. Waterfallagent/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→ повторная подача по ярусу.
- три яруса = в какую очередь положить: красный «прервать» делает сначала
Два железных правила в точке встречи
- Заморозка обязана позволить плагину сначала открыть ворота шага: ворота шага подвешены на
agent/pre-step, а пауза на уровне раунда ждёт события на безопасной границе — они ожидали бы друг друга (pauseTask/cancelTaskэтого плагина сначала делаютrelease); - Пока ворота шага закрыты, сообщения уже забраны:
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— настройки + доступность taskControlGET /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.
