dsh-autorestart 0.6.0 → 0.6.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/README.md +22 -5
- package/docs/README.zh.md +7 -3
- package/index.js +76 -16
- package/lib/semver.js +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ what it adds before you install it.
|
|
|
76
76
|
|
|
77
77
|
## Install
|
|
78
78
|
|
|
79
|
-
Current version: **0.6.
|
|
79
|
+
Current version: **0.6.1**.
|
|
80
80
|
|
|
81
81
|
From npm — the package is published under the name `dsh-autorestart`:
|
|
82
82
|
|
|
@@ -284,9 +284,25 @@ once, in the same composer dock, as a small card:
|
|
|
284
284
|
already shown (`seenFeatureVersions` in the checkpoint file). A fresh installation never sees the
|
|
285
285
|
card: the install question already explains the features, and answering it marks the announcement
|
|
286
286
|
as seen.
|
|
287
|
-
- **
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
- **"Seen" means "not older", not "equal".** The card counts as answered as soon as the recorded
|
|
288
|
+
version is **not older** than the current feature version — a record of `0.6.0` counts as seen
|
|
289
|
+
while the feature itself stands at `0.5.0`. Why not equality? Because the record does not say
|
|
290
|
+
*which* release the user answered on (until 0.6.0 the card carried the version of the package),
|
|
291
|
+
so an equality check would tell a user who already dismissed the card that they never saw it. A
|
|
292
|
+
record that is not a version at all counts as seen as well: a broken record must not make the
|
|
293
|
+
card come back for ever.
|
|
294
|
+
- **Once per feature version, for ever.** "Got it" records the version, the card disappears at once,
|
|
295
|
+
and it never comes back — not with the next restart of the host, and not with the next release of
|
|
296
|
+
the plugin (a *newer version of the feature* is announced again, of course).
|
|
297
|
+
- **The announcement follows the FEATURE, not the release.** The version it carries is a constant in
|
|
298
|
+
the source (`SLEEP_FEATURE_VERSION` in `index.js`), **not** the version of the package: a release
|
|
299
|
+
never brings the card back. Until 0.6.0 the card carried the package version, and every release
|
|
300
|
+
(0.5.0 → 0.6.0) brought "graceful shutdown" back to the users who had already answered it.
|
|
301
|
+
- **How to announce it again on purpose.** Raise `SLEEP_FEATURE_VERSION` to a version **newer** than
|
|
302
|
+
the one that was recorded last: the card is then shown exactly once more. Do that **only** when
|
|
303
|
+
the feature itself changed so much that the user has to hear about it again (the shutdown became a
|
|
304
|
+
completely different thing, the button moved somewhere else, …). A release, a bug fix or a small
|
|
305
|
+
addition must **not** touch it — that is exactly the bug this constant exists to prevent.
|
|
290
306
|
- **An unanswered card is not swallowed.** The card stays until it is answered, and it comes back with
|
|
291
307
|
the next start of DSH.
|
|
292
308
|
- **The countdown is read, not written.** The seconds in the card are the ones that are really in
|
|
@@ -294,7 +310,8 @@ once, in the same composer dock, as a small card:
|
|
|
294
310
|
("The button is currently switched off; you can turn it on in the settings").
|
|
295
311
|
- **Where the state lives.** `<DSH_HOME>/storages/dsh-autorestart/state.json`, next to the checkpoint
|
|
296
312
|
of the plugin. The host serves it on `GET /announcements` (`{ ok, pending: [{ feature, version }] }`)
|
|
297
|
-
and records it on `POST /announcements` (`{ feature }`).
|
|
313
|
+
and records it on `POST /announcements` (`{ feature }`). The `version` in that answer is the feature
|
|
314
|
+
version — the same number that `SLEEP_FEATURE_VERSION` holds.
|
|
298
315
|
|
|
299
316
|
## Graceful shutdown (收工关机)
|
|
300
317
|
|
package/docs/README.zh.md
CHANGED
|
@@ -98,10 +98,13 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
|
|
|
98
98
|
> **知道了**
|
|
99
99
|
|
|
100
100
|
- **只有升级才会看到**:宿主把「这个功能的版本」和「已经展示过的版本」比一比(记在检查点文件的 `seenFeatureVersions` 里)。**新装**的用户不会看到这张卡 —— 安装提问已经把功能讲清楚了,答完就等于"已看过"。
|
|
101
|
-
-
|
|
101
|
+
- **「看过」= 记录版本不低于当前功能版本,不是「等于」**:记录的版本只要**不低于**当前的功能版本就算看过 —— 功能停在 `0.5.0` 时,记录是 `0.6.0` 也算看过。**为什么不按等值判**:记录里写的是「当时公告带的版本」,而 0.6.0 之前公告带的是**包版本**,所以按等值判就会告诉一个早就点过「知道了」的用户「你还没见过」;那条记录只说明他在哪个版本上点过,不说明他没看过这个功能。记录根本不是版本号(比如坏记录)时也算看过 —— 宁可少打扰,也不要因为一条坏记录天天弹卡片。
|
|
102
|
+
- **每个功能版本只弹一次**:点「知道了」→ 宿主记下版本 → 卡片立刻消失 → **以后再也不出现**(重启宿主不会,升级发下一个版本也不会 —— 除非功能的版本比记录的更新,那时才再公告一次)。
|
|
103
|
+
- **公告跟的是「功能」,不是「发版」**:公告带的版本号是源码里的常量 `SLEEP_FEATURE_VERSION`(`index.js`),**不是** `package.json` 里的包版本 —— 所以发版不会把老用户已经点过「知道了」的卡片再弹回来。(0.6.0 之前公告带的是包版本,于是 0.5.0 → 0.6.0 每发一次版,「收工关机」就重弹一次。)
|
|
104
|
+
- **想主动再公告一次**:把 `SLEEP_FEATURE_VERSION` 调到**比最后记录的版本更新**的数即可 —— 卡片就再出现一次。但**只在功能本身变化大到必须再讲一遍时**才调(比如关机改成了完全不同的用法、按钮换了位置);**发版、修 bug、加小功能都不要动它** —— 那正是这个常量要防的坑。
|
|
102
105
|
- **没点不会静默吞掉**:卡片一直挂在那里等你回答;不回答,下次启动还会再出现一次。
|
|
103
106
|
- **秒数是读出来的,不是写死的**:卡片里的秒数就是当前生效的 `sleepCountdownSeconds`;月亮按钮被关掉时,卡片还会补一句「按钮目前关闭,可在设置里打开。」
|
|
104
|
-
- **状态存在哪**:`<DSH_HOME>/storages/dsh-autorestart/state.json`(和插件的检查点同一个文件)。宿主用 `GET /announcements` 报(`{ ok, pending: [{ feature, version }] }`),用 `POST /announcements`(body `{ feature }
|
|
107
|
+
- **状态存在哪**:`<DSH_HOME>/storages/dsh-autorestart/state.json`(和插件的检查点同一个文件)。宿主用 `GET /announcements` 报(`{ ok, pending: [{ feature, version }] }`),用 `POST /announcements`(body `{ feature }`)记。里面的 `version` 就是功能版本,跟 `SLEEP_FEATURE_VERSION` 是同一个数。
|
|
105
108
|
|
|
106
109
|
## 工作方式
|
|
107
110
|
|
|
@@ -134,7 +137,7 @@ agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autores
|
|
|
134
137
|
|
|
135
138
|
## 安装
|
|
136
139
|
|
|
137
|
-
当前版本:**0.6.
|
|
140
|
+
当前版本:**0.6.1**。
|
|
138
141
|
|
|
139
142
|
```powershell
|
|
140
143
|
# 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
|
|
@@ -515,6 +518,7 @@ lib/checkpoint.js 原子 JSON 检查点,容错读取,boot/restart/session
|
|
|
515
518
|
lib/lifecycle.js 纯决策:bootPlan / restartDecision / selectGoalDisarms / continuationPrompt / describeState
|
|
516
519
|
lib/restart.js Windows 脱离重启(WMI 主、schtasks 兜底、dryRun、自定义命令)
|
|
517
520
|
lib/sleep.js 收工关机:识别真宿主机(含 WSL)、关机命令、等待→倒数→关机的状态机
|
|
521
|
+
lib/semver.js 版本号比较(公告去重用:只看"记录版本是否低于当前功能版本")
|
|
518
522
|
locale/{en,zh}.json Plugin Manager 显示用标题/描述
|
|
519
523
|
icon.svg 插件图标
|
|
520
524
|
LICENSE MIT 许可(发布到插件市场需要)
|
package/index.js
CHANGED
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
runCommandNow,
|
|
76
76
|
scheduleDetachedRestart,
|
|
77
77
|
} from './lib/restart.js'
|
|
78
|
+
import { compareSemver } from './lib/semver.js'
|
|
78
79
|
import {
|
|
79
80
|
createSleepController,
|
|
80
81
|
COUNTDOWN_CHOICES,
|
|
@@ -133,17 +134,36 @@ function snapGraceChoice(value) {
|
|
|
133
134
|
* `pluginVersion()`), but a bundled installation may not ship that file — so
|
|
134
135
|
* the constant below is the last resort. A test keeps the two in sync.
|
|
135
136
|
*/
|
|
136
|
-
const PLUGIN_VERSION_FALLBACK = '0.6.
|
|
137
|
+
const PLUGIN_VERSION_FALLBACK = '0.6.1'
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The version of the graceful-shutdown FEATURE, not the version of the package.
|
|
141
|
+
*
|
|
142
|
+
* The one-time announcement is bound to this constant. It used to follow the
|
|
143
|
+
* version in package.json, and that made every release resurrect the card the
|
|
144
|
+
* user had already answered (0.5.0 -> 0.6.1 brought "收工关机" back). A release
|
|
145
|
+
* is not news about the feature, so the two are decoupled.
|
|
146
|
+
*
|
|
147
|
+
* WHEN TO RAISE IT: only when the feature itself changed so much that the
|
|
148
|
+
* user has to hear about it again (the shutdown became a completely different
|
|
149
|
+
* thing, the button moved to another place, ...). Any new value works: the
|
|
150
|
+
* recorded version no longer matches, so the announcement is shown once more.
|
|
151
|
+
*
|
|
152
|
+
* WHEN NOT TO RAISE IT: a release, a bug fix, a small addition, a new option
|
|
153
|
+
* that the documentation explains. None of those deserve a card that
|
|
154
|
+
* interrupts the user a second time.
|
|
155
|
+
*/
|
|
156
|
+
const SLEEP_FEATURE_VERSION = '0.5.0'
|
|
137
157
|
|
|
138
158
|
/** The cached version of this plugin (read once per process). */
|
|
139
159
|
let pluginVersionCache = null
|
|
140
160
|
|
|
141
161
|
/**
|
|
142
|
-
* The version of this plugin.
|
|
162
|
+
* The version of this plugin (the PACKAGE version).
|
|
143
163
|
*
|
|
144
|
-
* It is
|
|
145
|
-
*
|
|
146
|
-
*
|
|
164
|
+
* It says which release is installed - not what the user has been told about.
|
|
165
|
+
* The feature announcements are bound to `SLEEP_FEATURE_VERSION`, so a release
|
|
166
|
+
* never brings an answered announcement back.
|
|
147
167
|
*
|
|
148
168
|
* @returns {string}
|
|
149
169
|
*/
|
|
@@ -169,19 +189,51 @@ function pluginVersion() {
|
|
|
169
189
|
*
|
|
170
190
|
* The install prompt only ever reaches a fresh installation, so an
|
|
171
191
|
* installation that already existed would never learn about a new feature.
|
|
172
|
-
* Every entry of this list is therefore announced once per version:
|
|
173
|
-
* announcement is shown until the user presses "知道了", and the Host
|
|
174
|
-
* the version it has shown, so it never comes back.
|
|
192
|
+
* Every entry of this list is therefore announced once per FEATURE version:
|
|
193
|
+
* the announcement is shown until the user presses "知道了", and the Host
|
|
194
|
+
* records the feature version it has shown, so it never comes back.
|
|
195
|
+
*
|
|
196
|
+
* The version of an entry is the version of the FEATURE, never the version of
|
|
197
|
+
* the package - see `SLEEP_FEATURE_VERSION`.
|
|
175
198
|
*
|
|
176
199
|
* @returns {{ feature: string, version: string }[]}
|
|
177
200
|
*/
|
|
178
201
|
function featureAnnouncements() {
|
|
179
202
|
return [
|
|
180
203
|
// The graceful shutdown ("收工关机") and its moon button.
|
|
181
|
-
{ feature: 'sleep', version:
|
|
204
|
+
{ feature: 'sleep', version: SLEEP_FEATURE_VERSION },
|
|
182
205
|
]
|
|
183
206
|
}
|
|
184
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Whether the user has already seen the announcement of one feature.
|
|
210
|
+
*
|
|
211
|
+
* The record is the version that was announced when the user answered. It
|
|
212
|
+
* counts as seen as soon as it is NOT OLDER than the current feature version
|
|
213
|
+
* - not only when it is exactly that version:
|
|
214
|
+
*
|
|
215
|
+
* - the announcement used to carry the version of the PACKAGE, so a user
|
|
216
|
+
* who answered it on the release 0.6.1 recorded "0.6.1" while the feature
|
|
217
|
+
* itself stands at "0.5.0". With an equality check that record would
|
|
218
|
+
* look stale, and the very same card would be shown to a user who already
|
|
219
|
+
* dismissed it;
|
|
220
|
+
* - a record that is not a version at all counts as seen as well: a broken
|
|
221
|
+
* record must not make the card come back for ever.
|
|
222
|
+
*
|
|
223
|
+
* A missing record counts as NOT seen: a missing record must never swallow an
|
|
224
|
+
* announcement.
|
|
225
|
+
*
|
|
226
|
+
* @param {unknown} recorded the version in the state file
|
|
227
|
+
* @param {string} current the current feature version
|
|
228
|
+
* @returns {boolean} true when the user has seen it
|
|
229
|
+
*/
|
|
230
|
+
function isFeatureSeen(recorded, current) {
|
|
231
|
+
if (typeof recorded !== 'string' || recorded.trim() === '') return false
|
|
232
|
+
const order = compareSemver(recorded, current)
|
|
233
|
+
if (order === null) return true
|
|
234
|
+
return order >= 0
|
|
235
|
+
}
|
|
236
|
+
|
|
185
237
|
/**
|
|
186
238
|
* `@deepseek-ai/schemastery` resolves inside modules that live in the dsh installation,
|
|
187
239
|
* but NOT from this bundle: a profile-installed bundle is a symlink into the user's
|
|
@@ -2132,11 +2184,16 @@ const titleOfSummary = (item) => {
|
|
|
2132
2184
|
/**
|
|
2133
2185
|
* The feature announcements the user has not seen yet.
|
|
2134
2186
|
*
|
|
2135
|
-
* A feature is pending when the recorded version is
|
|
2136
|
-
* user who has seen the announcement of version 0.
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2187
|
+
* A feature is pending when the recorded version is OLDER than the current
|
|
2188
|
+
* one: a user who has seen the announcement of the feature version 0.5.0
|
|
2189
|
+
* never sees it again - not with the next release of the plugin, because
|
|
2190
|
+
* the announcement follows the FEATURE and not the package. A user who
|
|
2191
|
+
* answered the card on an intermediate release (the announcement used to
|
|
2192
|
+
* carry the package version) has seen it as well: a record that is not
|
|
2193
|
+
* older than the current version never brings the card back.
|
|
2194
|
+
*
|
|
2195
|
+
* A feature without a record is always pending - a missing record must
|
|
2196
|
+
* never swallow an announcement.
|
|
2140
2197
|
*
|
|
2141
2198
|
* @returns {{ feature: string, version: string }[]}
|
|
2142
2199
|
*/
|
|
@@ -2144,7 +2201,7 @@ const titleOfSummary = (item) => {
|
|
|
2144
2201
|
const seen = state.seenFeatureVersions && typeof state.seenFeatureVersions === 'object'
|
|
2145
2202
|
? state.seenFeatureVersions
|
|
2146
2203
|
: {}
|
|
2147
|
-
return featureAnnouncements().filter((entry) => seen[entry.feature]
|
|
2204
|
+
return featureAnnouncements().filter((entry) => !isFeatureSeen(seen[entry.feature], entry.version))
|
|
2148
2205
|
}
|
|
2149
2206
|
|
|
2150
2207
|
/**
|
|
@@ -3834,7 +3891,8 @@ function buildHandoffRoute(ctx, runtime, config) {
|
|
|
3834
3891
|
*
|
|
3835
3892
|
* The record lives in the checkpoint file, so an announcement that was shown
|
|
3836
3893
|
* once never comes back — not with the next boot and not with the next
|
|
3837
|
-
*
|
|
3894
|
+
* release of the plugin. Only a new FEATURE version (see
|
|
3895
|
+
* `SLEEP_FEATURE_VERSION`) brings it back, on purpose.
|
|
3838
3896
|
*/
|
|
3839
3897
|
function buildAnnouncementsRoute(ctx, runtime, config) {
|
|
3840
3898
|
const routePath = resolveAnnouncementsPath(config)
|
|
@@ -4967,4 +5025,6 @@ export {
|
|
|
4967
5025
|
resolveAnnouncementsPath,
|
|
4968
5026
|
featureAnnouncements,
|
|
4969
5027
|
pluginVersion,
|
|
5028
|
+
PLUGIN_VERSION_FALLBACK,
|
|
5029
|
+
SLEEP_FEATURE_VERSION,
|
|
4970
5030
|
}
|
package/lib/semver.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-autorestart — the smallest semver comparison this plugin needs.
|
|
3
|
+
*
|
|
4
|
+
* The feature announcements are de-duplicated by VERSION, not by equality: a
|
|
5
|
+
* record counts as "seen" as soon as it is not older than the current feature
|
|
6
|
+
* version. That comparison has to be numeric — the string comparison of
|
|
7
|
+
* "0.10.0" and "0.9.0" says the opposite of the truth, because "10" sorts
|
|
8
|
+
* before "9".
|
|
9
|
+
*
|
|
10
|
+
* Only the plain `x.y.z` triple is understood. Anything else (a build
|
|
11
|
+
* metadata suffix, a pre-release tag, a broken record) has no order at all,
|
|
12
|
+
* and the caller decides what to do with that.
|
|
13
|
+
*
|
|
14
|
+
* @module lib/semver
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A version is three numbers and nothing else. */
|
|
18
|
+
const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Split a version into its three numbers.
|
|
22
|
+
*
|
|
23
|
+
* @param {unknown} value
|
|
24
|
+
* @returns {[number, number, number]|null} null when it is not a version
|
|
25
|
+
*/
|
|
26
|
+
function parseVersion(value) {
|
|
27
|
+
if (typeof value !== 'string') return null
|
|
28
|
+
const match = VERSION_PATTERN.exec(value.trim())
|
|
29
|
+
if (!match) return null
|
|
30
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compare two versions numerically.
|
|
35
|
+
*
|
|
36
|
+
* @param {unknown} a the left version
|
|
37
|
+
* @param {unknown} b the right version
|
|
38
|
+
* @returns {-1|0|1|null} -1 when `a` is older, 1 when it is newer, 0 when they
|
|
39
|
+
* are the same, and null when one of them is not a version at all
|
|
40
|
+
*/
|
|
41
|
+
export function compareSemver(a, b) {
|
|
42
|
+
const left = parseVersion(a)
|
|
43
|
+
const right = parseVersion(b)
|
|
44
|
+
if (left === null || right === null) return null
|
|
45
|
+
for (let index = 0; index < 3; index += 1) {
|
|
46
|
+
if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1
|
|
47
|
+
}
|
|
48
|
+
return 0
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-autorestart",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepSeek Harness plugin: restart without losing work. Waits until every conversation is idle, restarts the host, then automatically resumes the interrupted sessions. AI-generated, not human-reviewed: use with caution.",
|
|
6
6
|
"license": "MIT",
|