dsh-autorestart 0.8.2 → 0.8.4
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.md +50 -1
- package/README.md +39 -14
- package/client.js +952 -56
- package/docs/README.zh.md +17 -10
- package/index.js +26 -7
- package/lib/checkpoint.js +23 -4
- package/lib/restart-phase.js +52 -11
- package/locale/en.json +26 -5
- package/locale/zh.json +26 -5
- package/package.json +1 -1
package/docs/README.zh.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- 多个会话同时要求重启时合并成一次,且每个请求者都会被唤醒;
|
|
21
21
|
- 带 goal 的会话不会把重启卡死:重启前自动**解除(disarm)**它的 goal(当前那一轮照常跑完,什么都不丢),重启之后自动重新上链;
|
|
22
22
|
- 重启前有两道检查:宿主侧查一次,独立 helper 在杀进程前再查一次;
|
|
23
|
-
- 界面上能看到状态:输入框工具行左侧的"待重启 · 等待 N
|
|
23
|
+
- 界面上能看到状态:输入框工具行左侧的"待重启 · 等待 N 个会话"小胶囊、左侧栏会话标题旁的圆点;快重启时圆点呼吸,点胶囊看详情;
|
|
24
24
|
- 可配置:静默窗口(默认 2 秒)、杀进程前等待(默认 2 秒)、忙碌时是否允许强制重启等。
|
|
25
25
|
- 同一套"等到大家都安静"的想法还做成了**收工关机**:左侧栏最底部(设置按钮旁边)一个月亮按钮 → 等所有会话收工 → 屏幕上倒数(一张浮动卡片:大字 + 鲸鱼娘)→ **关掉整台电脑**(见「收工关机」)。
|
|
26
26
|
|
|
@@ -133,7 +133,7 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
|
|
|
133
133
|
| --- | --- |
|
|
134
134
|
| Agent 工具(6 个) | `continuum_restart`、`continuum_status`、`continuum_cancel_restart`、`continuum_park_goal`、`continuum_recover_now`、`continuum_handoff` |
|
|
135
135
|
| HTTP 路由(7 条) | `<healthPath>`(默认 `/__dsh_autorestart`)下的 `GET /health`(另有 `?state=1`、`?gate=1`、`?probeIndex=1`)、`POST /park`、`POST /recover`、`POST /handoff`、`GET /sleep`、`POST /sleep`(收工关机——桌面端和 web 端走的是同一条路由)、`GET /announcements`、`POST /announcements`(一次性新功能公告);只接受本机回环或已认证的浏览器 |
|
|
136
|
-
| 前端槽位(6 个) | `conversation.input.left`(待重启小胶囊,id `autorestart-restart-pill`)、`sidebar.session.row.leading`(侧栏状态圆点,id `autorestart-restart-dot`)、`conversation.input.dock`(一次性安装提问,id `autorestart-backup-choice`;一次性新功能公告,id `autorestart-feature-announcement`)、`sidebar.footer.action`(收工关机的月亮按钮,id `autorestart-sleep-button`)、`shell.overlay
|
|
136
|
+
| 前端槽位(6 个) | `conversation.input.left`(待重启小胶囊,id `autorestart-restart-pill`)、`sidebar.session.row.leading`(侧栏状态圆点,id `autorestart-restart-dot`)、`conversation.input.dock`(一次性安装提问,id `autorestart-backup-choice`;一次性新功能公告,id `autorestart-feature-announcement`)、`sidebar.footer.action`(收工关机的月亮按钮,id `autorestart-sleep-button`)、`shell.overlay`(倒数浮动卡片,以及**主界面居中的重启阶段浮层**:`autorestart-sleep-overlay`、`autorestart-restart-overlay`)、`settings.section`(设置页里的「自动重启与关机」分区,id `dsh-autorestart`;保存回执也渲染在这个面板**内部**,不占窗口级浮层) |
|
|
137
137
|
| 系统提示词 | 一段「重启政策」说明,告诉 agent 怎么请求重启、插件会做什么 |
|
|
138
138
|
| 宿主事件 | `agent/status`(跟踪哪些会话在忙)、`tools/pre-execute`(拦下"手动杀/重启 DSH"的 shell 命令)、启动时的恢复流程 |
|
|
139
139
|
| profile 行 | 一条 bundle 行:`dsh-autorestart` |
|
|
@@ -330,7 +330,7 @@ agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autores
|
|
|
330
330
|
|
|
331
331
|
## 安装
|
|
332
332
|
|
|
333
|
-
当前版本:**0.8.
|
|
333
|
+
当前版本:**0.8.4**。
|
|
334
334
|
|
|
335
335
|
```powershell
|
|
336
336
|
# 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
|
|
@@ -364,9 +364,17 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
|
|
|
364
364
|
|
|
365
365
|
## 配置
|
|
366
366
|
|
|
367
|
-
|
|
367
|
+
设置页里关于「备份」「回执」和「启动方式」的几块,先说清楚它们看的是什么:
|
|
368
368
|
|
|
369
|
-
- **现在备份了什么**(在两个备份勾选的下面):宿主真的去读 `<DSH_HOME>/storages/dsh-autorestart/backups
|
|
369
|
+
- **现在备份了什么**(在两个备份勾选的下面):宿主真的去读 `<DSH_HOME>/storages/dsh-autorestart/backups/`,所以份数、每份的**本地时间**、大小、有没有含已装插件、以及存放路径都是真的。只有目录元信息被读,备份里的内容**不遍历**(一份就可能 150 MB)。没有备份时写的是「还没有备份;每次重启前会自动生成一份」。每份备份是**两行带标签的写法**,不再挤成一句:
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
备份时间 2026-10-29 22:08
|
|
373
|
+
备份大小 156 MB(含已装插件)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
时间就是拍快照那台机器的本地时间,**精确到分、不带时区偏移**(`+08:00` 只会添乱——看的人本来就看自己的钟)。大小自动选单位(`92 KB` / `156 MB`),只在有意义时保留一位小数(`117.7 MB`,永远不写 `156.0 MB`);「(含已装插件)」紧跟在大小后面,因为它正是这份备份为什么这么大。两份备份就是两组,组与组之间留空隙。
|
|
377
|
+
- **设置改动有回执**:每个开关/下拉都是改了就写,所以面板必须说清写没写进去——宿主回 `ok: true` 之后,**设置面板内部顶部**(面板自己的 `sticky` 条,往下滚动也一直可见)出现绿色**「已生效」** + 小字时间(`已生效 21:40:12`),3 秒后淡出;被拒绝时是红色**「没保存成功」** + 宿主给的原因,不自动消失,直到下一次改动成功或你自己点 × 关掉。**宿主没回话之前一律不显示**:首次渲染、轮询刷新、点控件都不会亮。
|
|
370
378
|
- **启动方式**(Web 托管端才有):默认只给**一行大白话**(例如「我记下的启动方式:由你的脚本拉起(重启交给它,我不插手)」)加两个小动作(重新检测 / 撤销);命令原文收在**「技术细节」**折叠里,默认收起,展开才出现。**桌面客户端这一整块不渲染** —— 它自己拉起自己,也不该写 `restartCommand`。宿主说不清是哪一端时,宁可整块不显示。
|
|
371
379
|
|
|
372
380
|
所有键都有默认值,写在 `cordis.patch.yml` 的 insert 行里;要覆盖就在 profile 的
|
|
@@ -529,11 +537,10 @@ restart gate: action=wait reason=agents-busy-within-soft-window busyCount=1 quie
|
|
|
529
537
|
|
|
530
538
|
### 小胶囊:点它看详情,也从这儿自己撤掉
|
|
531
539
|
|
|
532
|
-
输入框那排的小胶囊只有 22px
|
|
533
|
-
不放名字,也不放任何"双击可以取消"之类的提示(没地方放)。**单击**它(或聚焦后按 Enter / 空格)会在上方
|
|
540
|
+
输入框那排的小胶囊只有 22px 高、一行宽,所以它**只放一句短话**(`待重启 · 空闲后自动重启` / `待重启 · 等待 2 个会话`,≤12 字)——不放名字,也不放「双击可以取消」之类的提示(没地方放)。细节都下沉一层:长句只做 tooltip,**阶段**(备份中 / 重启中 / 已回来)显示在**主界面居中的浮层卡片**上,数量和「怎么撤」在点开的小面板里。**单击**它(或聚焦后按 Enter / 空格)会在上方
|
|
534
541
|
弹出一个小面板:
|
|
535
542
|
|
|
536
|
-
-
|
|
543
|
+
- 这次重启还在等几个会话;
|
|
537
544
|
- 还有几个**别的**会话也在等同一次重启;
|
|
538
545
|
- 一个按钮:「取消我的这次重启」。
|
|
539
546
|
|
|
@@ -933,8 +940,8 @@ or by requesting the health path with ?gate=1.
|
|
|
933
940
|
|
|
934
941
|
- 胶囊渲染在**输入框工具行左侧**(`conversation.input.left` 槽位),是一个高度约 22px 的小胶囊,**不额外占一行**(不再是输入框上方的一整行)。
|
|
935
942
|
- 只有当健康路由 `/__dsh_autorestart/health?state=1` 返回 `pendingRestart: true` **且**本会话 id 出现在 `waiters` 列表里时才会出现;别的会话等待重启时你看不到它。
|
|
936
|
-
-
|
|
937
|
-
- 悬停提示(原生 `title
|
|
943
|
+
- 文案有多种状态:还有别的**会话**占着时显示「待重启 · 等待 N 个会话」;都空闲时显示「待重启 · 空闲后自动重启」;被某个**自动续跑的 goal** 占着时显示「待重启 · 被某个自动续跑的 goal 占着」(见下面的升级 3)。
|
|
944
|
+
- 悬停提示(原生 `title`):「等待其他会话结束(计数含"排队但还没开始"的会话)后自动重启;重启后会自动继续本会话进度」。
|
|
938
945
|
- **每个占用者都说清自己是什么**:普通会话用它自己的标题;**子智能体**用"创建它的那个会话"的标题来称呼(显示成「子智能体(那个会话)」)。**原始的 `session-xxxx` 永远不会推给界面** —— 取不到标题时用一个通用说法。
|
|
939
946
|
- 颜色走**品牌色(蓝紫)** `var(--dsw-alias-brand-primary, #5b8def)`,**不再用告警黄(warning)色**:胶囊里的小圆点和侧边栏圆点都是品牌色,胶囊边框保持中性(`--dsw-alias-border-l1`),文字保持中性。
|
|
940
947
|
- 左侧边栏会话行的**行首 16px 状态格**里也会出现一个同款 8px 圆点(`sidebar.session.row.leading` 槽位,id `autorestart-restart-dot`,order 20):纯图标,完整说明放在悬停 `title` 里,aria-label 为「待重启」/「Pending restart」。
|
package/index.js
CHANGED
|
@@ -387,7 +387,7 @@ function snapGraceChoice(value, fallback = DEFAULT_RESTART_GRACE_MS) {
|
|
|
387
387
|
* `pluginVersion()`), but a bundled installation may not ship that file — so
|
|
388
388
|
* the constant below is the last resort. A test keeps the two in sync.
|
|
389
389
|
*/
|
|
390
|
-
const PLUGIN_VERSION_FALLBACK = '0.8.
|
|
390
|
+
const PLUGIN_VERSION_FALLBACK = '0.8.4'
|
|
391
391
|
|
|
392
392
|
/**
|
|
393
393
|
* The version of the graceful-shutdown FEATURE, not the version of the package.
|
|
@@ -2714,6 +2714,9 @@ const titleOfSummary = (item) => {
|
|
|
2714
2714
|
at: record && typeof record === 'object' ? record.at : null,
|
|
2715
2715
|
progress: record && typeof record === 'object' ? record.progress : null,
|
|
2716
2716
|
blocking: record && typeof record === 'object' && Number.isFinite(record.blocked) ? record.blocked : blocked,
|
|
2717
|
+
// The measured size of the pre-restart snapshot: it survives the stage
|
|
2718
|
+
// change, so a page that missed the copy can still report it.
|
|
2719
|
+
snapshotBytes: record && typeof record === 'object' ? record.snapshotBytes : null,
|
|
2717
2720
|
now: Date.now(),
|
|
2718
2721
|
})
|
|
2719
2722
|
}
|
|
@@ -4373,7 +4376,7 @@ const titleOfSummary = (item) => {
|
|
|
4373
4376
|
/**
|
|
4374
4377
|
* The stage of a restart, cut down for the health route (no long texts).
|
|
4375
4378
|
*
|
|
4376
|
-
* @returns {object} `{ phase, at, progress, slow, cancellable }`
|
|
4379
|
+
* @returns {object} `{ phase, at, blocking, progress, slow, cancellable }`
|
|
4377
4380
|
*/
|
|
4378
4381
|
restartStageBrief() {
|
|
4379
4382
|
const info = restartStageInfo()
|
|
@@ -4381,6 +4384,11 @@ const titleOfSummary = (item) => {
|
|
|
4381
4384
|
phase: info.phase,
|
|
4382
4385
|
at: info.at,
|
|
4383
4386
|
elapsedMs: info.elapsedMs,
|
|
4387
|
+
// How many tasks still hold the gate: the centred overlay prints the
|
|
4388
|
+
// number, so it travels as a number and not only inside `text`.
|
|
4389
|
+
blocking: info.blocking,
|
|
4390
|
+
// The size of the pre-restart snapshot, on every stage (bytes).
|
|
4391
|
+
snapshotBytes: info.snapshotBytes,
|
|
4384
4392
|
progress: info.progress === null
|
|
4385
4393
|
? null
|
|
4386
4394
|
: { files: info.progress.files, bytes: info.progress.bytes, total: info.progress.total, percent: info.progress.percent, ageMs: info.progress.ageMs },
|
|
@@ -7879,11 +7887,16 @@ function registerHealthRoute(ctx, runtime, config, sleep) {
|
|
|
7879
7887
|
// without one is sent exactly as the gate produced it. The goal
|
|
7880
7888
|
// reason (`goal-active` / `goal-exhausted`) is attached as well, so
|
|
7881
7889
|
// the client can tell a goal-driven blocker from a busy session.
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7890
|
+
// A blocker that is not an object (an empty string still reported by
|
|
7891
|
+
// an older route, a null) is not a blocker: it has no session and no
|
|
7892
|
+
// name, and rendering one produced the bare ",," the user saw. It is
|
|
7893
|
+
// dropped HERE, so the interface never receives one.
|
|
7894
|
+
const blockedBy = goalAwareBlockingDetails(runtime, verdict.blockedBy)
|
|
7895
|
+
.filter((entry) => entry && typeof entry === 'object')
|
|
7896
|
+
.map((entry) => {
|
|
7897
|
+
const title = runtime.sessionTitle(entry.sessionId)
|
|
7898
|
+
return title ? { ...entry, title } : entry
|
|
7899
|
+
})
|
|
7887
7900
|
stateBody = JSON.stringify({
|
|
7888
7901
|
ok: true,
|
|
7889
7902
|
pendingRestart,
|
|
@@ -7891,6 +7904,12 @@ function registerHealthRoute(ctx, runtime, config, sleep) {
|
|
|
7891
7904
|
generation: live.generation,
|
|
7892
7905
|
waiters,
|
|
7893
7906
|
blockedBy,
|
|
7907
|
+
// THE ONE NUMBER both client surfaces read: the capsule and the
|
|
7908
|
+
// panel it opens. Counting the blockers in the browser is what made
|
|
7909
|
+
// "1 个会话在忙" sit next to "还在等 0 个任务" — the two surfaces
|
|
7910
|
+
// counted different filtered copies of the same list (user report,
|
|
7911
|
+
// 2026-10-29). A blocker without a readable name is still a task.
|
|
7912
|
+
busyCount: countBlockerTasks(verdict.blockedBy),
|
|
7894
7913
|
allowRestart: verdict.allow === true,
|
|
7895
7914
|
// WHY the gate says what it says. Without this, "allowRestart is
|
|
7896
7915
|
// false and blockedBy is empty" looks like a contradiction: the
|
package/lib/checkpoint.js
CHANGED
|
@@ -223,6 +223,11 @@ function normalizeRestartPhase(raw, now) {
|
|
|
223
223
|
startedAt,
|
|
224
224
|
at,
|
|
225
225
|
progress,
|
|
226
|
+
// HOW BIG THE PRE-RESTART BACKUP WAS. It is kept across the stage change on
|
|
227
|
+
// purpose: a hosted profile can be 92 KB and the copy takes ~5 ms, so a page
|
|
228
|
+
// that polls every 2 s would otherwise never learn that a backup happened
|
|
229
|
+
// (user report, 2026-10-29). null = nothing was measured.
|
|
230
|
+
snapshotBytes: isFiniteNumber(raw.snapshotBytes) ? Math.max(0, Math.trunc(raw.snapshotBytes)) : null,
|
|
226
231
|
blocked: isFiniteNumber(raw.blocked) ? Math.max(0, Math.trunc(raw.blocked)) : 0,
|
|
227
232
|
};
|
|
228
233
|
return { record, changed };
|
|
@@ -246,10 +251,18 @@ export function setRestartPhase(state, { phase, now = Date.now(), blocked = 0, r
|
|
|
246
251
|
const previous = isObject(state.restartPhase) ? state.restartPhase : null;
|
|
247
252
|
// A new restart starts its own clock; a stage change inside the same restart
|
|
248
253
|
// keeps the start time, so "elapsed" is the whole restart and not one stage.
|
|
249
|
-
const
|
|
250
|
-
&& !(phase === 'waiting' && previous.phase !== 'waiting')
|
|
251
|
-
|
|
252
|
-
|
|
254
|
+
const sameRestart = restartStage && previous !== null && RESTART_PHASES.includes(previous.phase)
|
|
255
|
+
&& !(phase === 'waiting' && previous.phase !== 'waiting');
|
|
256
|
+
const startedAt = sameRestart ? previous.startedAt : moment;
|
|
257
|
+
// THE SIZE OF THE SNAPSHOT, measured once and then CARRIED: the live byte
|
|
258
|
+
// counter while the copy runs, the final size afterwards. A 92 KB profile is
|
|
259
|
+
// copied in ~5 ms, so this number is the only thing a polling interface can
|
|
260
|
+
// still say about a backup that really happened (user report, 2026-10-29).
|
|
261
|
+
const measuredBytes = previous !== null && isFiniteNumber(previous.snapshotBytes)
|
|
262
|
+
? Math.max(0, Math.trunc(previous.snapshotBytes))
|
|
263
|
+
: (previous !== null && isObject(previous.progress) && isFiniteNumber(previous.progress.bytes)
|
|
264
|
+
? Math.max(0, Math.trunc(previous.progress.bytes))
|
|
265
|
+
: null);
|
|
253
266
|
const record = {
|
|
254
267
|
phase,
|
|
255
268
|
startedAt,
|
|
@@ -259,6 +272,9 @@ export function setRestartPhase(state, { phase, now = Date.now(), blocked = 0, r
|
|
|
259
272
|
progress: phase === 'snapshotting' && previous !== null && previous.phase === 'snapshotting'
|
|
260
273
|
? previous.progress
|
|
261
274
|
: null,
|
|
275
|
+
// A NEW restart starts with nothing measured; a stage change inside one
|
|
276
|
+
// restart keeps the size it already has.
|
|
277
|
+
snapshotBytes: sameRestart ? measuredBytes : null,
|
|
262
278
|
blocked: isFiniteNumber(blocked) ? Math.max(0, Math.trunc(blocked)) : 0,
|
|
263
279
|
};
|
|
264
280
|
state.restartPhase = record;
|
|
@@ -289,6 +305,9 @@ export function setRestartProgress(state, { files = 0, bytes = 0, total = 0, fil
|
|
|
289
305
|
filesTotal: number(filesTotal),
|
|
290
306
|
at: moment,
|
|
291
307
|
},
|
|
308
|
+
// The live byte counter IS the size of the snapshot so far: keeping it here
|
|
309
|
+
// means the number survives the moment the copy ends.
|
|
310
|
+
snapshotBytes: number(bytes),
|
|
292
311
|
blocked: previous !== null && isFiniteNumber(previous.blocked) ? previous.blocked : 0,
|
|
293
312
|
};
|
|
294
313
|
state.updatedAt = moment;
|
package/lib/restart-phase.js
CHANGED
|
@@ -152,6 +152,7 @@ export function estimateRemainingSeconds({ bytes = null, total = null, elapsedMs
|
|
|
152
152
|
* cancellable: boolean,
|
|
153
153
|
* cancelReason: string|null,
|
|
154
154
|
* estimateSeconds: number|null,
|
|
155
|
+
* blocking: number|null,
|
|
155
156
|
* text: { zh: string, en: string },
|
|
156
157
|
* }}
|
|
157
158
|
*/
|
|
@@ -161,6 +162,7 @@ export function describeRestartPhase({
|
|
|
161
162
|
at = null,
|
|
162
163
|
progress = null,
|
|
163
164
|
blocking = null,
|
|
165
|
+
snapshotBytes = null,
|
|
164
166
|
now = Date.now(),
|
|
165
167
|
} = {}) {
|
|
166
168
|
const stage = isRestartPhase(phase) ? phase : 'idle';
|
|
@@ -206,10 +208,21 @@ export function describeRestartPhase({
|
|
|
206
208
|
|
|
207
209
|
const estimateSeconds = stage === 'snapshotting' ? seconds : null;
|
|
208
210
|
const done = measured === null ? null : formatBytes(measured.bytes);
|
|
211
|
+
// HOW MANY TASKS STILL HOLD THE GATE. The centred overlay says
|
|
212
|
+
// "还有 N 个会话" as a number of its own, so the count travels as a NUMBER
|
|
213
|
+
// and not only inside the sentence: a surface that has room for two words
|
|
214
|
+
// must not have to parse the long text.
|
|
215
|
+
//
|
|
216
|
+
// NULL means "the caller did not say", which is not the same as zero:
|
|
217
|
+
// Number(null) is 0, and a stage that was never given a count must not
|
|
218
|
+
// pretend the gate is free.
|
|
219
|
+
const blockingCount = blocking === null || blocking === undefined || blocking === ''
|
|
220
|
+
? null
|
|
221
|
+
: nonNegative(blocking);
|
|
209
222
|
|
|
210
223
|
let text;
|
|
211
224
|
if (stage === 'waiting') {
|
|
212
|
-
const count =
|
|
225
|
+
const count = blockingCount;
|
|
213
226
|
const suffixZh = count && count > 0 ? '(还有 ' + count + ' 个会话在忙)' : '';
|
|
214
227
|
const suffixEn = count && count > 0 ? ' (' + count + (count === 1 ? ' conversation' : ' conversations') + ' busy)' : '';
|
|
215
228
|
text = {
|
|
@@ -217,20 +230,36 @@ export function describeRestartPhase({
|
|
|
217
230
|
en: 'Waiting for every conversation to be idle…' + suffixEn,
|
|
218
231
|
};
|
|
219
232
|
} else if (stage === 'snapshotting') {
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
// ONE VOICE (user report, 2026-10-29): "正在备份" did not say WHAT was being
|
|
234
|
+
// backed up — the card says "DSH 重启前备份中", and this sentence (the
|
|
235
|
+
// settings page and the tooltip) says the same words, so no surface can
|
|
236
|
+
// disagree about what is running.
|
|
237
|
+
//
|
|
238
|
+
// The numbers appear ONLY when they were measured: "预计 X 秒" needs a real
|
|
239
|
+
// speed behind it, and "已完成 Y MB" a real byte count. With nothing
|
|
240
|
+
// measured yet the sentence claims nothing at all.
|
|
241
|
+
const measuredZh = [];
|
|
242
|
+
const measuredEn = [];
|
|
243
|
+
if (estimateSeconds !== null) {
|
|
244
|
+
measuredZh.push('预计 ' + estimateSeconds + ' 秒');
|
|
245
|
+
measuredEn.push('about ' + estimateSeconds + 's');
|
|
246
|
+
}
|
|
247
|
+
if (done !== null) {
|
|
248
|
+
measuredZh.push('已完成 ' + done);
|
|
249
|
+
measuredEn.push(done + ' done');
|
|
250
|
+
}
|
|
251
|
+
const detailZh = measuredZh.length === 0 ? '' : '(' + measuredZh.join(',') + ')';
|
|
252
|
+
const detailEn = measuredEn.length === 0 ? '' : ' (' + measuredEn.join(', ') + ')';
|
|
226
253
|
text = {
|
|
227
|
-
zh: '
|
|
228
|
-
en: 'Backing up
|
|
254
|
+
zh: 'DSH 重启前备份中' + detailZh + '…',
|
|
255
|
+
en: 'Backing up before DSH restart' + detailEn + '…',
|
|
229
256
|
};
|
|
230
257
|
if (slow !== null) {
|
|
258
|
+
// Still the same subject when it is slow: the user must know WHICH backup
|
|
259
|
+
// is slow, not only that "the backup" is.
|
|
231
260
|
text = {
|
|
232
|
-
zh: '
|
|
233
|
-
en: 'The backup is slower than expected (the disk may be busy); it is still running…'
|
|
261
|
+
zh: 'DSH 重启前备份比预期慢(可能磁盘很忙),还在继续…' + (done === null ? '' : '(已完成 ' + done + ')'),
|
|
262
|
+
en: 'The DSH pre-restart backup is slower than expected (the disk may be busy); it is still running…'
|
|
234
263
|
+ (done === null ? '' : ' (' + done + ' done)'),
|
|
235
264
|
};
|
|
236
265
|
}
|
|
@@ -258,6 +287,18 @@ export function describeRestartPhase({
|
|
|
258
287
|
cancellable: isCancellable(stage),
|
|
259
288
|
cancelReason: cancelRefusalReason(stage),
|
|
260
289
|
estimateSeconds,
|
|
290
|
+
// The number of tasks the gate still waits for, on its own: the centred
|
|
291
|
+
// overlay prints it, and a surface with room for two words must not have
|
|
292
|
+
// to parse the long sentence of `text`.
|
|
293
|
+
blocking: blockingCount,
|
|
294
|
+
// HOW BIG THE PRE-RESTART BACKUP IS (bytes). It is reported on EVERY stage
|
|
295
|
+
// of the restart, not only while the copy runs, so a card that appears a
|
|
296
|
+
// moment late can still say "backup done (92 KB)" instead of nothing.
|
|
297
|
+
// null means "nothing was measured", which is NOT the same as zero bytes:
|
|
298
|
+
// Number(null) is 0, and a receipt for a 0-byte backup would be a lie.
|
|
299
|
+
snapshotBytes: snapshotBytes === null || snapshotBytes === undefined || snapshotBytes === ''
|
|
300
|
+
? null
|
|
301
|
+
: nonNegative(snapshotBytes),
|
|
261
302
|
text,
|
|
262
303
|
};
|
|
263
304
|
}
|
package/locale/en.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"label": "Pending restart",
|
|
3
3
|
"busyPrefix": " · waiting for ",
|
|
4
|
-
"busySuffix": "
|
|
4
|
+
"busySuffix": " session(s)",
|
|
5
5
|
"idle": " · restarts once idle",
|
|
6
6
|
"goalActive": " · held by a self-continuing goal",
|
|
7
7
|
"goalExhausted": " · a goal ran out of rounds",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"parkBusy": "Parking…",
|
|
13
13
|
"parkError": "Park failed: {message}",
|
|
14
14
|
"tooltip": "Waits for the other sessions to become idle, then restarts automatically; this session resumes its progress after the restart.",
|
|
15
|
-
"tooltipBusy": "Waits for the other
|
|
15
|
+
"tooltipBusy": "Waits for the other sessions to finish (the count includes work that is queued but not started yet), then restarts automatically; this session resumes its progress after the restart.",
|
|
16
16
|
"tooltipWithError": "Waits for the other sessions to become idle, then restarts automatically; this session resumes its progress after the restart. {message}",
|
|
17
17
|
"dotAria": "Pending restart",
|
|
18
18
|
"panelHeading": "Pending restart",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"durationMinutesShort": "{n} min",
|
|
49
49
|
"durationHoursShort": "{n}h",
|
|
50
50
|
"durationHoursMinutesShort": "{n}h {m}m",
|
|
51
|
-
"cancelPanelTasks": "
|
|
51
|
+
"cancelPanelTasks": "Still waiting for {n} session(s)",
|
|
52
52
|
"cancelPanelOthers": "{n} other session(s) are waiting for this restart too",
|
|
53
53
|
"cancelPanelOthersAfter": "{n} session(s) are still waiting",
|
|
54
54
|
"cancelPanelAria": "Pending restart details",
|
|
@@ -201,8 +201,10 @@
|
|
|
201
201
|
"copyPath": "Copy the path",
|
|
202
202
|
"copied": "The path is on the clipboard",
|
|
203
203
|
"copyFailed": "The copy failed — please select the path above by hand",
|
|
204
|
-
"
|
|
205
|
-
"
|
|
204
|
+
"time": "Backup time",
|
|
205
|
+
"size": "Backup size",
|
|
206
|
+
"withPlugins": "(plugins included)",
|
|
207
|
+
"withoutPlugins": "(plugins excluded)"
|
|
206
208
|
},
|
|
207
209
|
"backupPlugins": {
|
|
208
210
|
"label": "Include the installed plugins in the backup",
|
|
@@ -299,6 +301,11 @@
|
|
|
299
301
|
"tierNone": "(this machine has no way to run it)"
|
|
300
302
|
},
|
|
301
303
|
"error": "Could not save the settings: {message}",
|
|
304
|
+
"saveNotice": {
|
|
305
|
+
"saved": "Saved",
|
|
306
|
+
"failed": "Not saved",
|
|
307
|
+
"dismiss": "Dismiss"
|
|
308
|
+
},
|
|
302
309
|
"autostartKind": {
|
|
303
310
|
"other": "something else",
|
|
304
311
|
"windows-task": "Windows scheduled task",
|
|
@@ -321,6 +328,20 @@
|
|
|
321
328
|
"self-supervised": "the supervisor of this plugin started it"
|
|
322
329
|
}
|
|
323
330
|
},
|
|
331
|
+
"restart": {
|
|
332
|
+
"stage": {
|
|
333
|
+
"snapshotting": "Backing up…",
|
|
334
|
+
"restarting": "Restarting…",
|
|
335
|
+
"back": "Back"
|
|
336
|
+
},
|
|
337
|
+
"overlay": {
|
|
338
|
+
"snapshotting": "Backing up before DSH restart",
|
|
339
|
+
"size": "{done} / {total}",
|
|
340
|
+
"sizeDone": "{done}",
|
|
341
|
+
"restarting": "Restarting…",
|
|
342
|
+
"back": "Back again"
|
|
343
|
+
}
|
|
344
|
+
},
|
|
324
345
|
"meta": {
|
|
325
346
|
"title": "dsh-autorestart — restart continuation",
|
|
326
347
|
"description": "Keeps agent work alive across a DeepSeek Harness restart: a durable checkpoint, goal re-arm or a continuation message on boot, and an idle-aware graceful restart the agent can request itself."
|
package/locale/zh.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"label": "待重启",
|
|
3
3
|
"busyPrefix": " · 等待 ",
|
|
4
|
-
"busySuffix": "
|
|
4
|
+
"busySuffix": " 个会话",
|
|
5
5
|
"idle": " · 空闲后自动重启",
|
|
6
6
|
"goalActive": " · 被某个自动续跑的 goal 占着",
|
|
7
7
|
"goalExhausted": " · 某个任务的 goal 轮次额度已用完,无法续跑",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"parkBusy": "停泊中…",
|
|
13
13
|
"parkError": "停泊失败:{message}",
|
|
14
14
|
"tooltip": "等待其他会话空闲后自动重启;重启后会自动继续本会话进度",
|
|
15
|
-
"tooltipBusy": "
|
|
15
|
+
"tooltipBusy": "等待其他会话结束(计数含“排队但还没开始”的会话)后自动重启;重启后本会话会自动继续进度",
|
|
16
16
|
"tooltipWithError": "等待其他会话空闲后自动重启;重启后本会话会自动继续进度。{message}",
|
|
17
17
|
"dotAria": "待重启",
|
|
18
18
|
"panelHeading": "待重启",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"durationMinutesShort": "{n} 分钟",
|
|
49
49
|
"durationHoursShort": "{n} 小时",
|
|
50
50
|
"durationHoursMinutesShort": "{n} 小时 {m} 分",
|
|
51
|
-
"cancelPanelTasks": "
|
|
51
|
+
"cancelPanelTasks": "还在等 {n} 个会话",
|
|
52
52
|
"cancelPanelOthers": "还有 {n} 个会话也在等这次重启",
|
|
53
53
|
"cancelPanelOthersAfter": "还剩 {n} 个会话在等",
|
|
54
54
|
"cancelPanelAria": "待重启详情",
|
|
@@ -201,8 +201,10 @@
|
|
|
201
201
|
"copyPath": "复制路径",
|
|
202
202
|
"copied": "路径已复制",
|
|
203
203
|
"copyFailed": "复制失败,请手动选中上面的路径",
|
|
204
|
-
"
|
|
205
|
-
"
|
|
204
|
+
"time": "备份时间",
|
|
205
|
+
"size": "备份大小",
|
|
206
|
+
"withPlugins": "(含已装插件)",
|
|
207
|
+
"withoutPlugins": "(不含已装插件)"
|
|
206
208
|
},
|
|
207
209
|
"backupPlugins": {
|
|
208
210
|
"label": "备份包含已安装插件",
|
|
@@ -299,6 +301,11 @@
|
|
|
299
301
|
"tierNone": "(这台机器没有可用机制)"
|
|
300
302
|
},
|
|
301
303
|
"error": "没能保存设置:{message}",
|
|
304
|
+
"saveNotice": {
|
|
305
|
+
"saved": "已生效",
|
|
306
|
+
"failed": "没保存成功",
|
|
307
|
+
"dismiss": "关闭"
|
|
308
|
+
},
|
|
302
309
|
"autostartKind": {
|
|
303
310
|
"other": "其他方式",
|
|
304
311
|
"windows-task": "Windows 计划任务",
|
|
@@ -321,6 +328,20 @@
|
|
|
321
328
|
"self-supervised": "它由本插件自己的守护在管"
|
|
322
329
|
}
|
|
323
330
|
},
|
|
331
|
+
"restart": {
|
|
332
|
+
"stage": {
|
|
333
|
+
"snapshotting": "重启前备份中",
|
|
334
|
+
"restarting": "正在重启…",
|
|
335
|
+
"back": "已回来"
|
|
336
|
+
},
|
|
337
|
+
"overlay": {
|
|
338
|
+
"snapshotting": "DSH 重启前备份中",
|
|
339
|
+
"size": "{done} / {total}",
|
|
340
|
+
"sizeDone": "{done}",
|
|
341
|
+
"restarting": "正在重启…",
|
|
342
|
+
"back": "已回来"
|
|
343
|
+
}
|
|
344
|
+
},
|
|
324
345
|
"meta": {
|
|
325
346
|
"title": "dsh-autorestart — 重启不断线",
|
|
326
347
|
"description": "让 DeepSeek Harness 重启后接着干活:落盘的检查点、启动时重新武装 goal 或给一条续接提示,以及一次等人闲下来的优雅重启。"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-autorestart",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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",
|