dsh-session-guard 0.1.2 → 0.2.0-beta.1
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/CHANGELOG.ja.md +75 -29
- package/CHANGELOG.ko.md +75 -29
- package/CHANGELOG.md +146 -29
- package/INSTALL.ja.md +83 -42
- package/INSTALL.ko.md +83 -42
- package/INSTALL.md +86 -44
- package/INSTALL.zh.md +82 -42
- package/README.en.md +303 -170
- package/README.ja.md +177 -106
- package/README.ko.md +177 -106
- package/README.md +159 -20
- package/dsh.plugin.json +13 -0
- package/lib/client.js +397 -11
- package/lib/client.js.map +1 -1
- package/package.json +101 -78
- package/src/bridge.js +61 -1
- package/src/client/badge-text.ts +49 -0
- package/src/client/index.ts +11 -1
- package/src/client/locales.ts +163 -95
- package/src/client/pause-button-text.ts +60 -0
- package/src/client/pause-button.tsx +129 -0
- package/src/client/settings-card.tsx +391 -191
- package/src/client/status-badge.tsx +59 -66
- package/src/client/styles.ts +30 -0
- package/src/deferrals.js +197 -0
- package/src/index.js +405 -293
- package/src/pause-gate.js +472 -416
- package/src/provider-directory.js +101 -0
- package/src/provider.js +139 -0
- package/src/request-guard.js +191 -0
- package/src/retry.js +227 -202
- package/src/settings.js +118 -97
- package/src/step-gate.js +399 -0
- package/src/targets.js +70 -0
- package/src/time.js +195 -104
- package/src/tool-call-id.js +44 -0
- package/src/wiring.js +305 -0
package/README.en.md
CHANGED
|
@@ -1,170 +1,303 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<strong>Peak auto session gate: weekend mode + peak auto-pause + session-level freeze + backend auto-retry</strong>
|
|
3
|
-
</p>
|
|
4
|
-
<p align="center">
|
|
5
|
-
<strong>English</strong> · <a href="README.md">中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a>
|
|
6
|
-
</p>
|
|
7
|
-
<p align="center">
|
|
8
|
-
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square"></a>
|
|
9
|
-
<img src="https://camo.githubusercontent.com/2c11fb2e0e14bb9985c5acbe61123a7441c5ee63aa27fa6e04e2a707ebfd6022/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6473682d2d706c7567696e2d72656164792d3437384342463f6c6f676f3d646565707365656b266c6f676f436f6c6f723d7768697465" alt="dsh-plugin" style="max-width: 100%;">
|
|
10
|
-
<img alt="Public beta" src="https://img.shields.io/badge/status-public%20beta-7da1de?style=flat-square">
|
|
11
|
-
</p>
|
|
12
|
-
|
|
13
|
-
# dsh-session-guard
|
|
14
|
-
|
|
15
|
-
- [English README](./README.en.md)
|
|
16
|
-
- [中文 README](./README.md)
|
|
17
|
-
- [日本語 README](./README.ja.md)
|
|
18
|
-
- [한국어 README](./README.ko.md)
|
|
19
|
-
- [Installation guide](./INSTALL.md)
|
|
20
|
-
- [中文安装指南](./INSTALL.zh.md)
|
|
21
|
-
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
22
|
-
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
23
|
-
- [Changelog](./CHANGELOG.md)
|
|
24
|
-
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
25
|
-
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
26
|
-
|
|
27
|
-
> **Compatibility note:** v0.1.1 ships Japanese (`ja`) and Korean (`ko`) dictionaries, but the current official DSH releases expose only `zh` and `en` through `LocaleRuntime`. On stock DSH, selecting `ja` or `ko` fails with `locale "<id>" is not registered`. These languages will work after official DSH adds the locale IDs. Advanced users can use a DSH fork that updates `LOCALE_IDS` and `LOCALES` labels, then rebuild.
|
|
28
|
-
|
|
29
|
-
>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- **
|
|
103
|
-
- **
|
|
104
|
-
- **
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
###
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- `
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
- `
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
|
163
|
-
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>Peak auto session gate: weekend mode + peak auto-pause + official-source guard + session-level freeze + backend auto-retry</strong>
|
|
3
|
+
</p>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<strong>English</strong> · <a href="README.md">中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a>
|
|
6
|
+
</p>
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-263146?style=flat-square"></a>
|
|
9
|
+
<img src="https://camo.githubusercontent.com/2c11fb2e0e14bb9985c5acbe61123a7441c5ee63aa27fa6e04e2a707ebfd6022/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6473682d2d706c7567696e2d72656164792d3437384342463f6c6f676f3d646565707365656b266c6f676f436f6c6f723d7768697465" alt="dsh-plugin" style="max-width: 100%;">
|
|
10
|
+
<img alt="Public beta" src="https://img.shields.io/badge/status-public%20beta-7da1de?style=flat-square">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
# dsh-session-guard
|
|
14
|
+
|
|
15
|
+
- [English README](./README.en.md)
|
|
16
|
+
- [中文 README](./README.md)
|
|
17
|
+
- [日本語 README](./README.ja.md)
|
|
18
|
+
- [한국어 README](./README.ko.md)
|
|
19
|
+
- [Installation guide](./INSTALL.md)
|
|
20
|
+
- [中文安装指南](./INSTALL.zh.md)
|
|
21
|
+
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
22
|
+
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
23
|
+
- [Changelog](./CHANGELOG.md)
|
|
24
|
+
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
25
|
+
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
26
|
+
|
|
27
|
+
> **Compatibility note:** v0.1.1 ships Japanese (`ja`) and Korean (`ko`) dictionaries, but the current official DSH releases expose only `zh` and `en` through `LocaleRuntime`. On stock DSH, selecting `ja` or `ko` fails with `locale "<id>" is not registered`. These languages will work after official DSH adds the locale IDs. Advanced users can use a DSH fork that updates `LOCALE_IDS` and `LOCALES` labels, then rebuild.
|
|
28
|
+
|
|
29
|
+
> **▼ DSH version compatibility**
|
|
30
|
+
>
|
|
31
|
+
> | DSH version | Load | Settings registration | Session events / gate | Client half |
|
|
32
|
+
> | --- | --- | --- | --- | --- |
|
|
33
|
+
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ✅ | `ctx.settings.register(ns, schema, { base })` | ✅ same shape | ✅ no platform value imports |
|
|
34
|
+
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ✅ | `register` still present (`installSection` added) | ✅ same shape | ✅ no platform value imports |
|
|
35
|
+
> | 0.1.3+ / 0.1.5-alpha.1 | APIs still present (unverified) | `register` unchanged | ✅ | ✅ |
|
|
36
|
+
>
|
|
37
|
+
> One artifact covers both. `session/event`, `agent.cancel`, `goals.pause`,
|
|
38
|
+
> `agent.followup`, `commands.register`, `timer.interval`, `webServer.register`,
|
|
39
|
+
> `agent/request`, `llm.listConfigurableProviders` and `settings.register/get`
|
|
40
|
+
> are signature-identical between `dsh-v0.1.1-rc.2` and `dsh-v0.1.2-rc.1`
|
|
41
|
+
> (verified through `0.1.5-alpha.1`). The one
|
|
42
|
+
> seam that needs a dual read is the `tool/result` call id (`content[].toolCallId`
|
|
43
|
+
> first, `source.callId` as fallback) — both forms appear in replay logs of both
|
|
44
|
+
> versions. It now lives in `src/tool-call-id.js` with unit tests.
|
|
45
|
+
> The `model/selection` event exists **only on 0.1.2+** and is feature-probed; the
|
|
46
|
+
> settings surface uses only the `register` + `get` intersection (never
|
|
47
|
+
> `installSection`, never the removed `installSettingsSection`). Drift guard:
|
|
48
|
+
> `tools/check-api-drift.ps1`.
|
|
49
|
+
|
|
50
|
+
> Automatically pause running sessions during peak pricing hours and resume during off-peak/weekend; pair with input-traffic's freeze button for **per-session** locking; backend **auto-retry** yields during freeze/gate. Core based on a custom session gate (`agent.cancel keepInbox + goals.pause + session/event safe boundary + followup resume`), no longer depending on dsh-task-control.
|
|
51
|
+
|
|
52
|
+
A cordis plugin assembled via the `dsh plugin` command and a bundle patch — no dsh source changes, no PR required.
|
|
53
|
+
|
|
54
|
+
> 💡 **Why recommended**: DeepSeek moved to **peak/off-peak billing** on 2026-08-17 — the peak window (Beijing time 09:00-12:00, 14:00-18:00) costs **2×** the off-peak rate. This plugin auto-pauses running sessions during peak and auto-resumes off-peak, saving up to **50%** on long-running sessions; manual freeze (via input-traffic button) provides per-session precision.
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **Weekend mode**: detects weekends (timezone-correct via `Intl.DateTimeFormat`, no裸 `getUTCDay()` Beijing-boundary 8-hour bug) → weekends ignore peak/off-peak, run freely.
|
|
59
|
+
- **Peak auto-pause (global)**: on peak entry (and not weekend), auto-pauses all running root sessions; off-peak auto-resumes all — **global switch, no manual action needed**.
|
|
60
|
+
- **Official-source guard (`providerGuard`)**: during peak hours only requests whose target is a DeepSeek official source are blocked; local/third-party providers (e.g. `local-35b`) keep running. Verdict order = explicit id list → `baseURL` endpoint → catalog default endpoint → builtin id.
|
|
61
|
+
- **Request-level backstop + deferral queue**: sessions started after peak entry, or switched to an official source mid-session, are caught by the `agent/request` guard (default `hold`: the request is suspended without an error and released off-peak).
|
|
62
|
+
- **Per-session freeze / resume**: `sessionGuard` redundant port + `POST /session-guard/rpc`, input-traffic freeze button per-session passthrough; also provides `/pause /resume /cancel` manual commands.
|
|
63
|
+
- **Backend auto-retry (D9)**: turn/end transient failures (error/429/max-tokens) auto-retry with adaptive backoff; permanent failures stop; **yields during freeze/gate**, never bypasses the session gate.
|
|
64
|
+
- **Fail-open**: custom session gate unavailable, session-guard not installed, settings service missing — all silently degrade, never crash on dependencies.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
dsh plugin --profile web add github:<owner>/dsh-session-guard
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Restart dsh web and refresh the page after installation.
|
|
73
|
+
|
|
74
|
+
## Settings (Settings → Plugins → session-guard, simple toggles)
|
|
75
|
+
|
|
76
|
+
| Toggle | Default | Description |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `enabled` | on | **Peak auto-pause**: auto-pause running sessions during peak hours |
|
|
79
|
+
| `stepLevelPause` | on | **Step-level gate**: during peak, hold *before* the next step's model request (earlier and cheaper than turn-level); off = fall back to turn-level pause |
|
|
80
|
+
| `providerGuard` | on | **Official-source guard**: block only DeepSeek official sources during peak; local/third-party providers keep running |
|
|
81
|
+
| `guardSubagents` | on | **Guard subagent requests**: subagent requests are billed too, guarded by default |
|
|
82
|
+
| `offPeakAutoResume` | on | **Off-peak auto-resume**: auto-resume paused sessions off-peak; off = no auto-resume (manual required) |
|
|
83
|
+
| `weekendMode` | on | **Weekend mode**: detect weekends → no auto-pause on weekends (no peak, run freely) |
|
|
84
|
+
| `deferredResume` | on | **Auto-resume after peak**: off = deferred requests/sessions stay parked until a manual `/resume` |
|
|
85
|
+
| `queueFallback` | on | Fallback to lock-wait queue when custom session gate is unavailable (fail-open) |
|
|
86
|
+
| `retryEnabled` | off | **Auto-retry (backend)**: transient failure auto-resume (off by default, conservative) |
|
|
87
|
+
|
|
88
|
+
Additional configuration:
|
|
89
|
+
|
|
90
|
+
- `timezone` (default Asia/Shanghai) — used for **weekend detection** and badge display; **does not affect peak/off-peak detection** (always Beijing time);
|
|
91
|
+
- `peakWindows` (default 09:00–12:00 / 14:00–18:00) — peak windows in Beijing time (UTC+8), matching DeepSeek's official billing;
|
|
92
|
+
- `pauseMode` (`safe`/`force`), `pauseReason` (`wait`/`stop`);
|
|
93
|
+
- `stepGateTimeoutMs` (default 300000) — step-gate hold timeout; on expiry the gate is released and the session **escalates to a turn-level pause** (anti-deadlock, and no "one step per 5 minutes" token drip);
|
|
94
|
+
- Official-source guard: `officialProviders` (extra official provider ids, comma separated, highest priority), `officialBaseURLs` (official endpoint hosts, default `api.deepseek.com`);
|
|
95
|
+
- Deferral queue: `deferredMode` (`hold` / `error`), `deferredResumeText`, `deferredMaxHoldMs` (hold cap, default 6h, then converts to an error);
|
|
96
|
+
- Retry parameters: `retryText`, `retryGraceMs`, `retryCooldownMs`, `retryBackoffFactor`, `retryBackoffMaxMs`, `retryMaxConsecutive`.
|
|
97
|
+
|
|
98
|
+
## Behavior
|
|
99
|
+
|
|
100
|
+
### Peak auto-gate (global)
|
|
101
|
+
|
|
102
|
+
- **Peak entry** (and not weekend): with `stepLevelPause` on, the running turn is **no longer interrupted** — the session runs to the next `agent/pre-step` boundary where the step gate holds it (see below); with it off, calls `gate.stopNextTurn` on all running root sessions (custom session gate truly pauses, or falls back to lock-wait queue per `queueFallback`);
|
|
103
|
+
- **Off-peak / weekend**: first `releaseAll` the held steps (the turn just continues in place), then `gate.resume` **all** sessions — controlled by `offPeakAutoResume`;
|
|
104
|
+
- **Peak timezone**: hardcoded to Beijing time (`Asia/Shanghai`), matching DeepSeek's official billing basis — not affected by the `timezone` setting;
|
|
105
|
+
- State machine: single-instance `NORMAL ↔ PAUSED_PEAK` (`scheduler.js`), driven by a single 30s tick.
|
|
106
|
+
|
|
107
|
+
### Step-level gate (v0.2.0, the token saver)
|
|
108
|
+
|
|
109
|
+
Hooks the `agent/pre-step` waterfall and holds the turn **before the next step's model request happens**.
|
|
110
|
+
|
|
111
|
+
- **Hold conditions** (all required): `enabled` + `stepLevelPause` + `step > 1` + peak (Beijing time, not weekend) + official target provider (`providerGuard`; all providers when off) + the session is not request-held + not manually bypassed this peak window;
|
|
112
|
+
- **Why `step > 1`**: the first step of a turn is covered by the request-level guard, so the two gates never overlap;
|
|
113
|
+
- **Release paths**: ① the "⏸ paused (resume)" button / `POST /session-guard/rpc {action:'stepResume'}` / `/resume` → lets the current step through and **stops gating this session for the rest of the peak window**; ② off-peak → release all, the turn continues in place (**no followup needed**); ③ freeze button / `/pause` / `/cancel` → release the gate and move to a turn-level pause; ④ `signal` abort → release;
|
|
114
|
+
- **Timeout escalation**: holding longer than `stepGateTimeoutMs` (default 5 min) releases the gate and **escalates to a turn-level force pause**, resumed off-peak (no deadlock, no token drip);
|
|
115
|
+
- **State**: `GET /session-guard/state?session=<id>` returns `paused: { step, turn }` and `stepGate: { held, since, bypass }`; the service port's `paused` **stays boolean** for compatibility, with `pausedStep` for the step gate;
|
|
116
|
+
- **Not persisted**: the hold is an in-process promise; a restart drops it (no ghost state).
|
|
117
|
+
|
|
118
|
+
#### Pause / resume session button (provided by session-guard)
|
|
119
|
+
|
|
120
|
+
The "Pause session" button in the composer's right row (slot `conversation.input.right`, id `session-guard-pause`, order 20, left of input-traffic's "❄ Freeze & append"):
|
|
121
|
+
|
|
122
|
+
- not paused → "Pause session", **clickable**: calls `stepPause` and pauses the session **before the next step's model request** (the current step is not interrupted; step 1 is held too, regardless of peak/provider);
|
|
123
|
+
- paused → "Resume session", calls `stepResume`: lets the current step through and stops gating this session for the rest of the peak window;
|
|
124
|
+
- **push updates**: `GET /session-guard/events?session=<id>` (SSE) pushes step-gate state changes **immediately** — when peak auto-holds, the button flips to "Resume session" without waiting for a poll; a 10s `/session-guard/state` poll remains as a fallback (SSE down → still converges);
|
|
125
|
+
- styled to match input-traffic's button in the same row (24px height / 6px radius / 12px font / same CSS tokens), with hover and paused states.
|
|
126
|
+
|
|
127
|
+
### Session locking (freeze)
|
|
128
|
+
|
|
129
|
+
- **Redundant port**: `ctx.provide('sessionGuard', service)` — `stopNextTurn(sessionId)` / `resume(sessionId)` / `lockQueue(sessionId)` / `unlockQueue(sessionId)` / `state(sessionId)`;
|
|
130
|
+
- **RPC bridge**: `POST /session-guard/rpc { action, sessionId }` — input-traffic freeze button calls `stopNextTurn` / `resume` per `sessionId`; silently skipped when session-guard is not installed (fail-open);
|
|
131
|
+
- **Manual commands**: `/pause [force|safe] [stop|wait]`, `/resume [confirm] [rerun|skip]`, `/cancel` —作用于调用它的会话.
|
|
132
|
+
|
|
133
|
+
### Backend auto-retry (D9)
|
|
134
|
+
|
|
135
|
+
Listens to `turn/end`, classifies failures:
|
|
136
|
+
|
|
137
|
+
- **Transient** (error/429/max-tokens) → adaptive backoff auto `followup(retryText)` resume;
|
|
138
|
+
- **Permanent** (auth/balance/model/context limit) → stop;
|
|
139
|
+
- **Yields during freeze/gate**: `isFrozen(sessionId)` true (queueLocked / paused / taskControl paused) → no retry;
|
|
140
|
+
- User intervention or successful turn resets consecutive failure count.
|
|
141
|
+
|
|
142
|
+
### Status badge (frontend display)
|
|
143
|
+
|
|
144
|
+
A **read-only** status badge is rendered on the right side of the composer input area, reflecting the current phase in real time:
|
|
145
|
+
|
|
146
|
+
| Phase | Label | CSS class | Meaning |
|
|
147
|
+
|---|---|---|---|
|
|
148
|
+
| `peak` (guard on) | 高峰·拦官方 | `sg-peak` | Peak hours; only DeepSeek official-source requests are blocked |
|
|
149
|
+
| `peak` (guard off) | 高峰·全部暂停 | `sg-peak` | Peak hours; every session is paused |
|
|
150
|
+
| `off-peak` | 谷时 | `sg-off` | Off-peak hours, sessions running normally |
|
|
151
|
+
| `weekend` | 周末 | `sg-weekend` | Weekend (when weekend mode is on), ignore peak/off-peak |
|
|
152
|
+
|
|
153
|
+
- **Polling**: requests `GET /session-guard/status` every 15 seconds for `phase`, `providerGuard`, `held`, `deferred`;
|
|
154
|
+
- **Fail-open**: route unreachable, network error, or `enabled` off → badge silently hidden, no session affected;
|
|
155
|
+
- **Independent of input-traffic**: the badge is rendered by session-guard's client code alone — **input-traffic is not required**. input-traffic only provides the freeze button, which is unrelated to the badge;
|
|
156
|
+
- **Tooltip**: hovering shows `phase · timezone · weekend mode · verdict scope · held/deferred counts`.
|
|
157
|
+
|
|
158
|
+
### Official-source detection (`providerGuard`)
|
|
159
|
+
|
|
160
|
+
During peak hours the plugin does not blanket-pause sessions: it first decides whether **the route this request will actually use** is a DeepSeek official source.
|
|
161
|
+
|
|
162
|
+
| Priority | Basis | `matchedBy` | Example |
|
|
163
|
+
|---|---|---|---|
|
|
164
|
+
| 1 | `officialProviders` explicit id list | `explicit` | user declares a self-hosted gateway as official |
|
|
165
|
+
| 2 | live `baseURL` normalized to a host | `endpoint` | `deepseek-official` pointed at a relay → **not blocked** |
|
|
166
|
+
| 3 | catalog builtin default endpoint | `endpoint-default` | pi-ai's `deepseek` route defaults to the official API → **blocked** |
|
|
167
|
+
| 4 | builtin id list (`deepseek-official`) | `route-id` | fallback when no endpoint is readable |
|
|
168
|
+
| 5 | anything else | `unknown` | not official, pass |
|
|
169
|
+
|
|
170
|
+
- **Endpoint beats id**: a route named `deepseek-official` whose `baseURL` points at a relay is **not** mis-blocked; conversely pi-ai's builtin `deepseek` route is **not** missed.
|
|
171
|
+
- **Endpoint source**: `ctx.get('llm').listConfigurableProviders()` → directory entry → `ctx.settings.get(settingsNs)` → `baseURL` via `settingsPath` (non-secret fields only; `apiKeyEnv` values are never read). Recomputed per request, never cached → provider config edits apply immediately.
|
|
172
|
+
- **Switching away auto-resumes**: if a session paused at peak entry is switched to a local/third-party provider (the `model/selection` event on 0.1.2+), it is resumed automatically (subject to `deferredResume`); only sessions this plugin paused at peak entry are touched — a manual `/pause` is never overridden. On 0.1.1 there is no such event, so this degrades to "next request or manual `/resume`".
|
|
173
|
+
- **When the endpoint is unreadable**: missing `llm` service, changed namespace shape, non-string field — degrade to id / builtin-endpoint verdict, record `matchedBy`, **never throw**.
|
|
174
|
+
- **Troubleshooting a wrong verdict**: `GET /session-guard/provider?provider=<id>` returns `{ official, matchedBy, endpoint }`.
|
|
175
|
+
|
|
176
|
+
### Request-level guard and the deferral queue
|
|
177
|
+
|
|
178
|
+
- **Why request level**: the 30s tick only handles sessions that were `running` at the `NORMAL → PAUSED_PEAK` transition; sessions started after peak entry, or switched to an official source mid-session, slip through. The `agent/request` waterfall runs on **every request**.
|
|
179
|
+
- **Verdict uses `next()`'s return value**: the model-selection middleware overrides provider/model inside the waterfall, so we must `await next()` before deciding.
|
|
180
|
+
- **`hold` mode (default)**: the request is suspended — **not sent, no error** — and released at the exact off-peak instant (`msUntilOffPeak` timer, 30s tick as backstop); abort cancels it normally.
|
|
181
|
+
- **`error` mode**: throws a recognizable `PEAK_DEFERRED` error + records a deferral, then resumes off-peak with `deferredResumeText` (or stays parked when `deferredResume` is off).
|
|
182
|
+
- **Cap**: `deferredMaxHoldMs` (default 6h) converts a still-held request into an error instead of hanging forever.
|
|
183
|
+
- **Mutual exclusion**: while a request is held the plugin **never** also asks the session gate to pause (a pause waits for a safe boundary that a held request can never reach). Sessions already held are skipped on peak entry.
|
|
184
|
+
- **No persistence**: the deferral queue is an in-process promise; a restart drops it.
|
|
185
|
+
|
|
186
|
+
### Boundaries (explicitly out of scope)
|
|
187
|
+
|
|
188
|
+
- **No provider rerouting**: this plugin blocks, it does not switch models.
|
|
189
|
+
- **Compaction does not go through `agent/request`**: it cannot happen while a session is paused; a manually triggered compaction during peak may still hit the official source (we do not intercept `ctx.llm.stream`).
|
|
190
|
+
- **0.1.1 has no `model/selection` event**: auto-recovery after switching to a non-official source degrades to "next request or manual `/resume`" (immediate on 0.1.2+).
|
|
191
|
+
- **No new npm dependencies**, no credential reads, and `dsh-llm-retry`'s 429 / transport retry behavior is untouched.
|
|
192
|
+
|
|
193
|
+
### Timezone handling
|
|
194
|
+
|
|
195
|
+
- **Peak/off-peak detection**: always uses **Beijing time (UTC+8)** via `BILLING_TIMEZONE = 'Asia/Shanghai'`, matching DeepSeek's official billing basis. This is **hardcoded** and not affected by the `timezone` setting;
|
|
196
|
+
- **Weekend detection**: uses the user-configured `timezone` (e.g. `Asia/Tokyo`, `Asia/Seoul`), because "weekend" is a local concept;
|
|
197
|
+
- `Intl.DateTimeFormat` is used for timezone projection — invalid IANA timezone names throw `RangeError`, caught by fail-open and falling back to `Asia/Shanghai`;
|
|
198
|
+
- Peak windows are **left-closed, right-open** `[start, end)`, supporting cross-midnight windows (e.g. `22:00–06:00`);
|
|
199
|
+
- The `timezone` setting works identically across all UI languages (zh/en/ja/ko) — IANA timezone names are locale-independent.
|
|
200
|
+
|
|
201
|
+
### Division of labour with input-traffic: one "stops", one "orders"
|
|
202
|
+
|
|
203
|
+
They act on **different links of the same chain**, and the boundary is set by DSH's own inbox model:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
user input ──(input-traffic picks the tier)──▶ next-step / next-turn pending queues
|
|
207
|
+
│
|
|
208
|
+
agent/pre-step ──(this plugin's step gate)──▶ pass / hold
|
|
209
|
+
│
|
|
210
|
+
agent/request ──(this plugin's request hold)──▶ pass / hold
|
|
211
|
+
│
|
|
212
|
+
model call
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**DSH queue semantics (two queues — don't mix them up)**
|
|
216
|
+
|
|
217
|
+
| Queue | Meaning | Consumed when |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `next-step` | "Input awaiting the next step boundary" | The next `agent/pre-step`: **same level as a tool result**, another step inside the same turn |
|
|
220
|
+
| `next-turn` | "Prompts awaiting individual turns" | After the current turn closes, as a **new turn** |
|
|
221
|
+
|
|
222
|
+
`Inbox.claim()` **always drains `next-step` first**, and only additionally takes **one** `next-turn` when that boundary opens a new turn; a turn's first step reads next-turn, every later step reads next-step.
|
|
223
|
+
|
|
224
|
+
**Ownership**
|
|
225
|
+
|
|
226
|
+
- **session-guard = stop**: decides *when progress may happen*, and **never touches queue content or order**.
|
|
227
|
+
- step gate (`agent/pre-step`): holds **before** the next step's model request;
|
|
228
|
+
- turn-level pause (`agent.cancel({keepInbox:true})` + `goals.pause` + safe boundary): stops the turn, **queue preserved as-is**;
|
|
229
|
+
- request-level guard (`agent/request` hold): holds **this one model request**.
|
|
230
|
+
- **input-traffic = order**: decides *which queue user input goes to, at what tier, and when it is consumed*.
|
|
231
|
+
- three tiers = which queue: red "interrupt" calls `cancel()` then `steer`; yellow "steer" calls `steer` (→ `next-step`, the same turn's next step); green "queue" stays in `next-turn`;
|
|
232
|
+
- freeze = detach all `queued` + `steering` rows (tiers preserved) + composer block + call `sessionGuard.stopNextTurn`; resume = clear the block → `sessionGuard.resume` first → re-submit by tier.
|
|
233
|
+
|
|
234
|
+
**Two invariants at the meeting point**
|
|
235
|
+
|
|
236
|
+
1. **Freeze must let this plugin release the step gate first**: the step gate sits at `agent/pre-step` while a turn-level pause waits for a safe-boundary event — they would wait on each other (`pauseTask` / `cancelTask` release it first);
|
|
237
|
+
2. **While the step gate is held, messages are already claimed**: `preStep()` calls `inbox.claim()` *before* dispatching the waterfall, so new input queues behind the claimed batch; `keepInbox` only applies to turn-level pauses.
|
|
238
|
+
|
|
239
|
+
**No crossing over**: input-traffic does not listen to `agent/pre-step` / `agent/request` (the only exception is the "interrupt" tier's explicit `cancel()`, which the user asked for); this plugin never rewrites `next-step` / `next-turn` content or order.
|
|
240
|
+
|
|
241
|
+
Buttons: this plugin's "Pause session / Resume session" (order 20) and input-traffic's "❄ Freeze & append / Resume & append" (order 30) sit side by side and replace neither — the former owns the step gate, the latter owns queue detach + turn-level freeze.
|
|
242
|
+
|
|
243
|
+
## Redundant port `sessionGuard`
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
{
|
|
247
|
+
stopNextTurn(sessionId, opts),
|
|
248
|
+
resume(sessionId, opts),
|
|
249
|
+
lockQueue(sessionId, reason),
|
|
250
|
+
unlockQueue(sessionId),
|
|
251
|
+
stepPause(sessionId), // request a manual step-level pause (held at the next pre-step)
|
|
252
|
+
stepResume(sessionId, opts), // release the step gate (v0.2.0); opts.bypass=false keeps gating this peak
|
|
253
|
+
state(sessionId), // { queueLocked, lockReason, paused, pausedStep, stepHeldSince, stepBypass, ... }
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## HTTP routes
|
|
258
|
+
|
|
259
|
+
- `GET /session-guard/state?session=<id>` — session state (`paused: { step, turn, manual }` / `stepGate` / last target / held / deferred)
|
|
260
|
+
- `GET /session-guard/events?session=<id>` — **SSE**: pushes step-gate state changes immediately (drives the button)
|
|
261
|
+
- `GET /session-guard/settings` — settings + taskControl availability
|
|
262
|
+
- `GET /session-guard/status` — global current phase (status badge polling; includes `stepHeld`)
|
|
263
|
+
- `GET /session-guard/provider?provider=<id>` — official-source verdict diagnostics (`official` / `matchedBy` / `endpoint`)
|
|
264
|
+
- `GET /session-guard/diag` — runtime diagnostics (includes `stepGate`)
|
|
265
|
+
- `POST /session-guard/rpc` — `{ action: stopNextTurn|resume|lockQueue|unlockQueue|stepPause|stepResume|state, sessionId }`
|
|
266
|
+
|
|
267
|
+
## State storage
|
|
268
|
+
|
|
269
|
+
Per-session JSON: `$DSH_HOME/.dsh/session-guard/<sessionId>.json` (atomic write; `DSH_SESSION_GUARD_STATE_DIR` override).
|
|
270
|
+
|
|
271
|
+
## Tests
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
npm test # node --test tests/*.test.mjs (timezone/weekend/state-machine/session-gate/bridge/retry)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Modules
|
|
278
|
+
|
|
279
|
+
| File | Responsibility |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `src/time.js` | Peak/weekend detection (timezone-correct) + `msUntilOffPeak` (exact release timing) |
|
|
282
|
+
| `src/scheduler.js` | Pure state machine NORMAL ↔ PAUSED_PEAK |
|
|
283
|
+
| `src/provider.js` | Official-source verdict (pure: endpoint normalization + decision matrix) |
|
|
284
|
+
| `src/provider-directory.js` | Endpoint directory (`llm.listConfigurableProviders` + `settings.get`, full degradation) |
|
|
285
|
+
| `src/deferrals.js` | Deferral registry (hold / release / cap / `PeakDeferredError`) |
|
|
286
|
+
| `src/request-guard.js` | `agent/request` request-level guard (hold / error modes) |
|
|
287
|
+
| `src/step-gate.js` | **`agent/pre-step` step-level gate** (v0.2.0: hold / release / timeout escalation / bypass; pure `decideStepHold`) |
|
|
288
|
+
| `src/targets.js` | Per-session "last real target" tracking (`request/header` + `model/selection`) |
|
|
289
|
+
| `src/wiring.js` | Wiring/orchestration (peak-entry filter / step-gate wiring / off-peak release / exact timer / dispose) |
|
|
290
|
+
| `src/pause-gate.js` | Custom session gate engine (releases the step gate before pausing) |
|
|
291
|
+
| `src/pause-store.js` | Custom pause state persistence |
|
|
292
|
+
| `src/gate.js` | Session gate driver (custom true pause / fallback lock queue, fail-open) |
|
|
293
|
+
| `src/bridge.js` | `sessionGuard` redundant port |
|
|
294
|
+
| `src/retry.js` | Backend auto-retry (classification/backoff/freeze yield; short-circuits only the exact `PEAK_DEFERRED` code) |
|
|
295
|
+
| `src/detect.js` | Auto-detection (host taskControl / client input-traffic bridge) |
|
|
296
|
+
| `src/store.js` | Per-session persistent state |
|
|
297
|
+
| `src/settings.js` | Settings sub-panel (schemastery schema + fail-open registration) |
|
|
298
|
+
| `src/index.js` | Host apply (settings/routes/tick/provide service/retry + request guard wiring) |
|
|
299
|
+
| `src/client/` | Browser half (status badge + settings card + four-language dictionaries) |
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT — see [LICENSE](LICENSE).
|