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 CHANGED
@@ -76,7 +76,7 @@ what it adds before you install it.
76
76
 
77
77
  ## Install
78
78
 
79
- Current version: **0.6.0**.
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
- - **Once per version, for ever.** "Got it" records the version, the card disappears at once, and it
288
- never comes back — not with the next restart of the host, and not with the next version of the
289
- plugin (a *new* version of a feature is announced again, of course).
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.0**。
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.0'
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 the version of the package itself: the feature announcements are
145
- * bound to it, so a new version of the plugin can announce a new feature (and
146
- * the announcement of an old version never comes back).
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: the
173
- * announcement is shown until the user presses "知道了", and the Host records
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: pluginVersion() },
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 not the current one: a
2136
- * user who has seen the announcement of version 0.6.0 never sees it again,
2137
- * and a user who upgrades to a newer version sees the announcement of that
2138
- * version. A feature without a record is always pending — a missing record
2139
- * must never swallow an announcement.
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] !== entry.version)
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
- * version of the plugin.
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.0",
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",