dsh-autorestart 0.5.0 → 0.6.0
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 +108 -5
- package/client.js +158 -20
- package/cordis.patch.yml +8 -0
- package/docs/README.zh.md +63 -6
- package/index.js +454 -27
- package/lib/checkpoint.js +78 -0
- package/lib/sleep.js +217 -33
- package/locale/en.json +8 -1
- package/locale/zh.json +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,10 +29,14 @@ round starts, so the session goes idle and the restart can proceed. The goal is
|
|
|
29
29
|
restart and continues.
|
|
30
30
|
|
|
31
31
|
The browser shows the state: a compact "pending restart" pill on the left side of the input toolbar
|
|
32
|
-
and a small dot next to the session title in the sidebar. The pill's text reports how many
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
breathes when the restart is about to fire.
|
|
32
|
+
and a small dot next to the session title in the sidebar. The pill's text reports how many **tasks**
|
|
33
|
+
are still in the way — a conversation, a subagent or a teammate counts as one task each, and the
|
|
34
|
+
tooltip explains that queued-but-not-started work counts too; it does not open a details panel. The
|
|
35
|
+
dot breathes when the restart is about to fire.
|
|
36
|
+
|
|
37
|
+
Every blocker says **what it is**: a conversation is named by its own title, and a **subagent** is
|
|
38
|
+
named after the conversation that created it ("Subagent of *that* conversation"). A raw session id is
|
|
39
|
+
never shown to the user.
|
|
36
40
|
|
|
37
41
|
Before every restart the plugin snapshots the profile, so a restart that leaves the host unable to
|
|
38
42
|
boot is rolled back automatically — see
|
|
@@ -72,7 +76,7 @@ what it adds before you install it.
|
|
|
72
76
|
|
|
73
77
|
## Install
|
|
74
78
|
|
|
75
|
-
Current version: **0.
|
|
79
|
+
Current version: **0.6.0**.
|
|
76
80
|
|
|
77
81
|
From npm — the package is published under the name `dsh-autorestart`:
|
|
78
82
|
|
|
@@ -140,6 +144,7 @@ show up in the Plugin Manager and in the config editor.
|
|
|
140
144
|
| `restartCommand` | `''` | Custom restart command. Supports the `{pid}`, `{ppid}` and `{app}` placeholders. |
|
|
141
145
|
| `relaunchDelaySeconds` | `2` | How long the detached helper waits before it kills the app (`ping -n 3`, roughly 2 seconds). |
|
|
142
146
|
| `settleMs` | `2000` | Quiet window in milliseconds: the host must stay silent this long after the last conversation activity before a restart may start. |
|
|
147
|
+
| `restartGraceMs` | `10000` | Grace window in milliseconds: how long the host waits after the last conversation finished before it is really restarted or switched off — a dropdown: `0` / `3` / `5` / `10` / `20` / `30` seconds. `0` means "no extra wait". |
|
|
143
148
|
| `autoReloadPage` | `true` | `dsh web`: reload the page automatically when the host boot token changes. |
|
|
144
149
|
| `blockManualRestart` | `true` | Hard-block an agent that tries to kill or restart DSH with a shell command, and point it at `continuum_restart` instead. |
|
|
145
150
|
| `healthPath` | `/__dsh_autorestart/health` | Health check route. Returns the boot id, the pid, the phase and the self-check flags. |
|
|
@@ -161,6 +166,30 @@ The two waits are independent of each other, and both default to two seconds:
|
|
|
161
166
|
Both can be overridden separately in the profile config. A larger `settleMs` is the more
|
|
162
167
|
conservative choice, because it is less likely to cut a session short that has just started.
|
|
163
168
|
|
|
169
|
+
### Why the host waits a few more seconds after the work is done
|
|
170
|
+
|
|
171
|
+
**The blind spot.** This plugin can only see the **DSH agents**: it knows whether a conversation is
|
|
172
|
+
running, whether it has queued inbox work, and when the last of them went idle. It cannot see the
|
|
173
|
+
work other plugins queue with a timer of their own — a memory plugin that writes its notes a few
|
|
174
|
+
seconds after the conversation ended, a summary, an MCP callback, a log flush. That work is
|
|
175
|
+
invisible here, so "every agent is idle" does not mean "nothing is happening any more".
|
|
176
|
+
|
|
177
|
+
**The grace window.** `restartGraceMs` (default **10 seconds**) is the extra wait for exactly
|
|
178
|
+
that: after the LAST conversation finished, the host waits this long before it is really restarted
|
|
179
|
+
or switched off. It is a plain, configurable insurance — not a special case for any particular
|
|
180
|
+
plugin.
|
|
181
|
+
|
|
182
|
+
- **Both paths use it.** The restart gate waits `max(settleMs, restartGraceMs)` of quiet, and the
|
|
183
|
+
graceful shutdown waits the same grace **before** its countdown starts (so the grace never pushes
|
|
184
|
+
a countdown that is already running back by a round).
|
|
185
|
+
- **New activity restarts the timer.** If a conversation starts working again while the host is
|
|
186
|
+
waiting, the grace starts over — the wait always follows the last activity.
|
|
187
|
+
- **`0` means "as before".** With `restartGraceMs: 0` the behaviour is exactly the one of 0.4.0.
|
|
188
|
+
- **Where to change it.** Settings → Auto-restart & shut down → *Wait a few more seconds after the
|
|
189
|
+
work is done*. The status interfaces report it too: `GET /health?state=1`, `GET /health?gate=1`,
|
|
190
|
+
`continuum_status` and `GET /sleep` all carry `graceMs` (the configured window) and `quietForMs`
|
|
191
|
+
(how long the host has actually been quiet).
|
|
192
|
+
|
|
164
193
|
### Platform note
|
|
165
194
|
|
|
166
195
|
`restartMode: app` only applies to the DeepSeek Harness desktop app. A hosted `dsh web`
|
|
@@ -276,6 +305,80 @@ be cancelled — the card has a button, and the moon button itself becomes a tog
|
|
|
276
305
|
is armed. If a conversation starts working again during the countdown, the countdown goes back to
|
|
277
306
|
"waiting" and nothing is switched off.
|
|
278
307
|
|
|
308
|
+
### Shutdown and restart: who goes first
|
|
309
|
+
|
|
310
|
+
**The short version.** Pressing the moon button means "switch the machine off when the work is
|
|
311
|
+
done" — **not** "switch it off right now, whatever else is running". So:
|
|
312
|
+
|
|
313
|
+
1. **A pending restart wins.** If a restart is still on the list, the shutdown does **not** happen.
|
|
314
|
+
The card says *"Waiting for the pending restart to finish before shutting down"*. The restart
|
|
315
|
+
goes first, the sessions come back with it and keep working, and only then does the machine
|
|
316
|
+
go off.
|
|
317
|
+
2. **The work keeps running after the restart.** The sessions that resume with the restart are
|
|
318
|
+
running work: while any of them is busy, the shutdown waits. The machine only goes off when
|
|
319
|
+
**every** session is idle again.
|
|
320
|
+
3. **The shutdown intent survives the restart.** You pressed the button **once**. The restart does
|
|
321
|
+
not swallow that request: it is written into the checkpoint file, and the new boot picks it up
|
|
322
|
+
and keeps waiting for the work. You do **not** have to press the button a second time.
|
|
323
|
+
4. **You can cancel at any time.** The waiting card has a *"Keep it on"* button; pressing it (or the
|
|
324
|
+
moon button again) forgets the intent for good.
|
|
325
|
+
|
|
326
|
+
**Where the intent lives.** In the checkpoint file
|
|
327
|
+
(`<DSH_HOME>/storages/dsh-autorestart/state.json`) as `sleep: { armed, armedAt, countdownSeconds }`.
|
|
328
|
+
It is cleared as soon as the machine went off, when the shutdown failed, and when you cancel. A
|
|
329
|
+
record that cannot be read is dropped — asking once more is much better than a machine that switches
|
|
330
|
+
itself off for ever because of a half-written file.
|
|
331
|
+
|
|
332
|
+
**For the troubleshooting.** `GET /sleep` reports `armedAt` (when the shutdown was armed) next to
|
|
333
|
+
`phase`, `blockers` and `graceMs`.
|
|
334
|
+
|
|
335
|
+
### Troubleshooting: the machine did not switch off
|
|
336
|
+
|
|
337
|
+
**These are the real answers of a running host** (the copy of the host is Chinese, the fields are
|
|
338
|
+
not):
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
POST /__dsh_autorestart/sleep {"action":"arm"} # three conversations are working
|
|
342
|
+
→ phase=waiting armed=true remainingMs=20000 (the full value: the countdown has NOT started)
|
|
343
|
+
message: 等 3 个任务收工,收工后开始倒数。
|
|
344
|
+
|
|
345
|
+
# after a restart was requested:
|
|
346
|
+
GET /__dsh_autorestart/sleep
|
|
347
|
+
→ phase=waiting armed=true remainingMs=20000
|
|
348
|
+
message: 等这次重启完成后再关机……
|
|
349
|
+
blockers: [{"sessionId":"restart-pending","kind":"restart-pending",
|
|
350
|
+
"label":"待执行的重启","name":"待执行的重启"}]
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`remainingMs` at its full value is the tell: **the countdown has not started**, so the machine is
|
|
354
|
+
waiting for something. Here is what it can be, and what to do about it:
|
|
355
|
+
|
|
356
|
+
1. **Some tasks are still working.** `blockers[]` lists them; look at `kind`:
|
|
357
|
+
`session` is a conversation, `subagent` is a child agent of one. A subagent counts as work on
|
|
358
|
+
purpose — otherwise a restart would cut it off. **What to do:** let it finish, or stop that
|
|
359
|
+
conversation. The list shrinks on its own.
|
|
360
|
+
2. **A restart is still pending.** `kind` is `restart-pending` (`label`/`name` are
|
|
361
|
+
`待执行的重启`). That is normal queueing, not a fault: the restart runs first, the sessions
|
|
362
|
+
come back with it, and the countdown continues afterwards. **What to do:** nothing — wait for
|
|
363
|
+
the restart. (You can also cancel the restart with `continuum_cancel_restart`.)
|
|
364
|
+
3. **The work just ended and the grace window is still running** (default 10 seconds, so that a
|
|
365
|
+
memory or summary plugin can still write its notes). **What you see:** `graceMs` is the
|
|
366
|
+
configured window, `quietForMs` is how long the host has actually been quiet. **What to do:**
|
|
367
|
+
wait; if another conversation starts working during the wait, the timer starts over (that is
|
|
368
|
+
the point of the window). Set it to `0` in the settings if you do not want the extra wait.
|
|
369
|
+
4. **Somebody started working during the countdown.** The countdown goes back to "waiting" and
|
|
370
|
+
`remainingMs` returns to its full value. **What you see:** the message says that somebody
|
|
371
|
+
started working again, and `blockers[]` lists who. **What to do:** let them finish.
|
|
372
|
+
5. **You want to know who it is waiting for.** `continuum_status` → `blockedBy[]` and
|
|
373
|
+
`GET /sleep` → `blockers[]` are the **same data**: the first is for agents, the second for
|
|
374
|
+
scripts. Each entry has `kind` (what it is), `label` (the title of the session) and `name`
|
|
375
|
+
(the whole name, e.g. "the subagent of the coding session"). A session whose title could not
|
|
376
|
+
be resolved reports `label: null` and an empty `name` — that is why the waiting line then
|
|
377
|
+
only says "等 3 个任务收工" without a list of names.
|
|
378
|
+
6. **You do not want it at all.** The cancel button on the waiting card (or the moon button
|
|
379
|
+
again, or `POST /sleep {"action":"cancel"}`) clears the intent for good: `armed` becomes
|
|
380
|
+
`false` and `armedAt` becomes `null`.
|
|
381
|
+
|
|
279
382
|
**Where the button is.** The moon button sits at the **very bottom of the left sidebar, right next
|
|
280
383
|
to the Settings row** (`sidebar.footer.action`). That seat is a **root** slot, so the button is
|
|
281
384
|
there even when no conversation is open — on any page. When the sidebar is collapsed into its narrow
|
package/client.js
CHANGED
|
@@ -114,20 +114,33 @@ const SLEEP_DEFAULT_COUNTDOWN_SECONDS = 20
|
|
|
114
114
|
/** How long the note of a finished rehearsal stays on screen (ms). */
|
|
115
115
|
const SLEEP_DRY_RUN_NOTICE_MS = 8_000
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The grace window the settings page offers (a dropdown, in seconds).
|
|
119
|
+
*
|
|
120
|
+
* It is the extra wait after the last conversation finished, so that work
|
|
121
|
+
* other plugins queue with a timer (a memory plugin writing its notes, a
|
|
122
|
+
* summary) still gets its chance — this plugin cannot see that work.
|
|
123
|
+
*/
|
|
124
|
+
const GRACE_CHOICES_SECONDS = Object.freeze([0, 3, 5, 10, 20, 30])
|
|
125
|
+
|
|
126
|
+
/** The grace window used when the Host did not report one (ms / seconds). */
|
|
127
|
+
const GRACE_DEFAULT_MS = 10_000
|
|
128
|
+
const GRACE_DEFAULT_SECONDS = 10
|
|
129
|
+
|
|
117
130
|
const ZH = {
|
|
118
131
|
label: '待重启',
|
|
119
|
-
busyPrefix: ' ·
|
|
120
|
-
busySuffix: '
|
|
132
|
+
busyPrefix: ' · 等待 ',
|
|
133
|
+
busySuffix: ' 个任务结束',
|
|
121
134
|
idle: ' · 空闲后自动重启',
|
|
122
|
-
goalActive: ' ·
|
|
123
|
-
goalExhausted: ' ·
|
|
135
|
+
goalActive: ' · 被某个自动续跑的 goal 占着',
|
|
136
|
+
goalExhausted: ' · 某个任务的 goal 轮次额度已用完,无法续跑',
|
|
124
137
|
parkedGoals: ' · 已停泊 {n} 个 goal,重启后自动继续',
|
|
125
138
|
parkButton: '停泊并重启',
|
|
126
139
|
parkButtonAria: '停泊该会话的 goal,并立即请求重启',
|
|
127
140
|
parkBusy: '停泊中…',
|
|
128
141
|
parkError: '停泊失败:{message}',
|
|
129
142
|
tooltip: '等待其他会话空闲后自动重启;重启后会自动继续本会话进度',
|
|
130
|
-
tooltipBusy: '
|
|
143
|
+
tooltipBusy: '等待其他任务结束(计数含“排队但还没开始”的任务)后自动重启;重启后本会话会自动继续进度',
|
|
131
144
|
tooltipWithError: '等待其他会话空闲后自动重启;重启后本会话会自动继续进度。{message}',
|
|
132
145
|
dotAria: '待重启',
|
|
133
146
|
panelHeading: '待重启',
|
|
@@ -136,7 +149,13 @@ const ZH = {
|
|
|
136
149
|
panelStateGoalActive: '等待其他会话停泊它自动续跑的 goal',
|
|
137
150
|
panelStateGoalExhausted: '等待其他会话的 goal(轮次额度已用完)',
|
|
138
151
|
panelBlockedTitle: '占用会话:{title}',
|
|
139
|
-
panelBlockedGeneric: '
|
|
152
|
+
panelBlockedGeneric: '有一个任务正在运行',
|
|
153
|
+
// One line per blocker: a session and a subagent are told apart, and
|
|
154
|
+
// neither of them is ever shown as a raw session id.
|
|
155
|
+
panelBlockedSession: '会话「{label}」正在运行',
|
|
156
|
+
panelBlockedSubagent: '{name}正在运行',
|
|
157
|
+
panelBlockedSubagentGeneric: '有一个子智能体正在运行',
|
|
158
|
+
panelBlockedRestartPending: '等这次重启完成后再关机',
|
|
140
159
|
panelGoalActiveLine: '占用会话「{title}」:goal 正在自动续跑(goal-active)',
|
|
141
160
|
panelGoalExhaustedLine: '占用会话「{title}」:goal 轮次额度已用完,无法继续(goal-exhausted)',
|
|
142
161
|
panelParkedTitle: '已停泊的 goal(重启后自动继续):',
|
|
@@ -177,6 +196,10 @@ const ZH = {
|
|
|
177
196
|
'settings.seconds.immediate': '不倒数(立即关机)',
|
|
178
197
|
'settings.seconds.value': '{n} 秒',
|
|
179
198
|
'settings.seconds.hint': '按下按钮后,等所有会话收工,再倒数这么多秒才关机;「不倒数」表示会话一收工就关机。',
|
|
199
|
+
'settings.grace.label': '收工后再等几秒(给记忆/总结类插件留时间)',
|
|
200
|
+
'settings.grace.hint': '最后一个会话收工后,再等这么久才真正重启或关机——我们只看得见 DSH 的会话,看不见别的插件用定时器排队的活。等待期间如果又有会话开始干活,会重新计时。',
|
|
201
|
+
'settings.grace.value': '{n} 秒',
|
|
202
|
+
'settings.grace.immediate': '不等(立即重启/关机)',
|
|
180
203
|
'settings.backup.label': '重启前备份',
|
|
181
204
|
'settings.backup.hint': '每次重启前先快照一次当前配置;万一重启后起不来,能自动回退。',
|
|
182
205
|
'settings.backupPlugins.label': '备份包含已安装插件',
|
|
@@ -212,7 +235,8 @@ const ZH = {
|
|
|
212
235
|
'sleep.countdown.hint': '再过 {n} 秒就关机,点「算了,别关」可以留下。',
|
|
213
236
|
'sleep.waiting.title': '还在等大家收工',
|
|
214
237
|
'sleep.waiting.body': '等这些会话忙完,就轮到关机啦:',
|
|
215
|
-
'sleep.waiting.count': '还有 {n}
|
|
238
|
+
'sleep.waiting.count': '还有 {n} 个任务在干活',
|
|
239
|
+
'sleep.waiting.subagents': '其中 {k} 个是子智能体',
|
|
216
240
|
'sleep.waiting.blocker': '· {title}',
|
|
217
241
|
'sleep.waiting.none': '(正在确认还有谁在忙)',
|
|
218
242
|
'sleep.waiting.cancel': '不关了',
|
|
@@ -246,8 +270,8 @@ const ZH = {
|
|
|
246
270
|
|
|
247
271
|
const EN = {
|
|
248
272
|
label: 'Pending restart',
|
|
249
|
-
busyPrefix: ' ·
|
|
250
|
-
busySuffix: '
|
|
273
|
+
busyPrefix: ' · waiting for ',
|
|
274
|
+
busySuffix: ' task(s) to finish',
|
|
251
275
|
idle: ' · restarts once idle',
|
|
252
276
|
goalActive: ' · held by a self-continuing goal',
|
|
253
277
|
goalExhausted: ' · a goal ran out of rounds',
|
|
@@ -259,7 +283,7 @@ const EN = {
|
|
|
259
283
|
tooltip:
|
|
260
284
|
'Waits for the other sessions to become idle, then restarts automatically; this session resumes its progress after the restart.',
|
|
261
285
|
tooltipBusy:
|
|
262
|
-
'Waits for the other
|
|
286
|
+
'Waits for the other tasks to finish (the count includes work that is queued but not started yet), then restarts automatically; this session resumes its progress after the restart.',
|
|
263
287
|
tooltipWithError:
|
|
264
288
|
'Waits for the other sessions to become idle, then restarts automatically; this session resumes its progress after the restart. {message}',
|
|
265
289
|
dotAria: 'Pending restart',
|
|
@@ -269,7 +293,13 @@ const EN = {
|
|
|
269
293
|
panelStateGoalActive: 'Waiting for the other sessions to park their self-continuing goal',
|
|
270
294
|
panelStateGoalExhausted: 'Waiting for the other sessions\' goal (its round budget is used up)',
|
|
271
295
|
panelBlockedTitle: 'Blocking session: {title}',
|
|
272
|
-
panelBlockedGeneric: 'One
|
|
296
|
+
panelBlockedGeneric: 'One task is running',
|
|
297
|
+
// One line per blocker: a session and a subagent are told apart, and
|
|
298
|
+
// neither of them is ever shown as a raw session id.
|
|
299
|
+
panelBlockedSession: 'Session "{label}" is running',
|
|
300
|
+
panelBlockedSubagent: '{name} is running',
|
|
301
|
+
panelBlockedSubagentGeneric: 'One subagent is running',
|
|
302
|
+
panelBlockedRestartPending: 'Waiting for the pending restart to finish before shutting down',
|
|
273
303
|
panelGoalActiveLine: 'Blocking session "{title}": the goal keeps driving rounds by itself (goal-active)',
|
|
274
304
|
panelGoalExhaustedLine: 'Blocking session "{title}": the goal ran out of rounds and cannot continue (goal-exhausted)',
|
|
275
305
|
panelParkedTitle: 'Parked goals (they continue after the restart):',
|
|
@@ -310,6 +340,10 @@ const EN = {
|
|
|
310
340
|
'settings.seconds.immediate': 'No countdown (switch off right away)',
|
|
311
341
|
'settings.seconds.value': '{n} seconds',
|
|
312
342
|
'settings.seconds.hint': 'After the button: wait for every conversation to be done, count down this many seconds and switch off. "No countdown" switches off as soon as the conversations are done.',
|
|
343
|
+
'settings.grace.label': 'Wait a few more seconds after the work is done',
|
|
344
|
+
'settings.grace.hint': 'After the last conversation finished, wait this long before the host is really restarted or switched off — this plugin only sees the DSH conversations, not the work other plugins queue with a timer. If a conversation starts working again while waiting, the timer starts over.',
|
|
345
|
+
'settings.grace.value': '{n} seconds',
|
|
346
|
+
'settings.grace.immediate': 'Do not wait (restart/switch off right away)',
|
|
313
347
|
'settings.backup.label': 'Backup before a restart',
|
|
314
348
|
'settings.backup.hint': 'Take a snapshot of the current configuration before every restart, so a host that cannot boot again is rolled back automatically.',
|
|
315
349
|
'settings.backupPlugins.label': 'Include the installed plugins in the backup',
|
|
@@ -345,7 +379,8 @@ const EN = {
|
|
|
345
379
|
'sleep.countdown.hint': 'Powering off in {n} seconds — pick "Never mind, keep it on" to stay.',
|
|
346
380
|
'sleep.waiting.title': 'Waiting for everyone to finish',
|
|
347
381
|
'sleep.waiting.body': 'Once these sessions are done, the machine powers off:',
|
|
348
|
-
'sleep.waiting.count': '{n}
|
|
382
|
+
'sleep.waiting.count': '{n} task(s) still working',
|
|
383
|
+
'sleep.waiting.subagents': '{k} of them are subagents',
|
|
349
384
|
'sleep.waiting.blocker': '· {title}',
|
|
350
385
|
'sleep.waiting.none': '(checking who is still busy)',
|
|
351
386
|
'sleep.waiting.cancel': 'Keep it on',
|
|
@@ -731,6 +766,53 @@ if (moduleLoader) moduleLoader.load({
|
|
|
731
766
|
))
|
|
732
767
|
}
|
|
733
768
|
|
|
769
|
+
/**
|
|
770
|
+
* The line of one blocker: a session and a subagent are told apart, and
|
|
771
|
+
* neither of them is ever shown as a raw session id.
|
|
772
|
+
*
|
|
773
|
+
* The Host says WHAT the blocker is (`kind`) and HOW it is called: a
|
|
774
|
+
* session by its own title (`label`), a subagent by the whole phrase
|
|
775
|
+
* (`name`: "写代码的会话的子智能体"). The phrase is used as it is — it
|
|
776
|
+
* reads as a sentence, while a bracketed label would read like an
|
|
777
|
+
* annotation. A subagent whose parent title is unknown has no name of its
|
|
778
|
+
* own, so it gets the generic line instead of an empty bracket.
|
|
779
|
+
*
|
|
780
|
+
* @param {any} entry the blocker the Host reported
|
|
781
|
+
* @returns {string}
|
|
782
|
+
*/
|
|
783
|
+
function blockerLine(entry) {
|
|
784
|
+
const t = translate ?? ((name) => name)
|
|
785
|
+
const kind = entry && entry.kind === 'subagent'
|
|
786
|
+
? 'subagent'
|
|
787
|
+
: (entry && entry.kind === 'restart-pending' ? 'restart-pending' : 'session')
|
|
788
|
+
const label = entry && typeof entry.label === 'string' ? entry.label.trim() : ''
|
|
789
|
+
if (kind === 'restart-pending') {
|
|
790
|
+
// A pending restart goes first: the shutdown waits for it.
|
|
791
|
+
return t('panelBlockedRestartPending')
|
|
792
|
+
}
|
|
793
|
+
if (kind === 'subagent') {
|
|
794
|
+
const name = entry && typeof entry.name === 'string' ? entry.name.trim() : ''
|
|
795
|
+
if (name === '' || label === '') return t('panelBlockedSubagentGeneric')
|
|
796
|
+
return fill(t('panelBlockedSubagent'), { name })
|
|
797
|
+
}
|
|
798
|
+
return label === ''
|
|
799
|
+
? t('panelBlockedGeneric')
|
|
800
|
+
: fill(t('panelBlockedSession'), { label })
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Whether a blocker is a pending RESTART (and not a task that is running).
|
|
805
|
+
*
|
|
806
|
+
* The restart is not a task: it is the thing the shutdown waits for, so it
|
|
807
|
+
* is never counted as one.
|
|
808
|
+
*
|
|
809
|
+
* @param {any} entry
|
|
810
|
+
* @returns {boolean}
|
|
811
|
+
*/
|
|
812
|
+
function isRestartPending(entry) {
|
|
813
|
+
return Boolean(entry) && entry.kind === 'restart-pending'
|
|
814
|
+
}
|
|
815
|
+
|
|
734
816
|
/** How long the pending restart has been waiting, as a locale string. */
|
|
735
817
|
function formatWaiting(ms, t) {
|
|
736
818
|
const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000))
|
|
@@ -2296,6 +2378,15 @@ if (moduleLoader) moduleLoader.load({
|
|
|
2296
2378
|
const backupIncludePlugins = status && typeof status === 'object' && typeof status.backupIncludePlugins === 'boolean'
|
|
2297
2379
|
? status.backupIncludePlugins
|
|
2298
2380
|
: true
|
|
2381
|
+
// The grace window: how long the host waits after the last conversation
|
|
2382
|
+
// finished. The Host reports it in milliseconds; the dropdown works in
|
|
2383
|
+
// seconds.
|
|
2384
|
+
const reportedGraceMs = status && typeof status === 'object' && typeof status.graceMs === 'number'
|
|
2385
|
+
? status.graceMs
|
|
2386
|
+
: GRACE_DEFAULT_MS
|
|
2387
|
+
const graceSeconds = GRACE_CHOICES_SECONDS.indexOf(Math.round(reportedGraceMs / 1000)) === -1
|
|
2388
|
+
? GRACE_DEFAULT_SECONDS
|
|
2389
|
+
: Math.round(reportedGraceMs / 1000)
|
|
2299
2390
|
|
|
2300
2391
|
/**
|
|
2301
2392
|
* Write one change into the profile configuration.
|
|
@@ -2363,6 +2454,33 @@ if (moduleLoader) moduleLoader.load({
|
|
|
2363
2454
|
),
|
|
2364
2455
|
h('span', { className: 'dsh-autorestart-settings__hint' }, t('settings.seconds.hint')),
|
|
2365
2456
|
),
|
|
2457
|
+
h(
|
|
2458
|
+
'label',
|
|
2459
|
+
{ className: 'dsh-autorestart-settings__field' },
|
|
2460
|
+
h('span', { className: 'dsh-autorestart-settings__label' }, t('settings.grace.label')),
|
|
2461
|
+
h(
|
|
2462
|
+
'select',
|
|
2463
|
+
{
|
|
2464
|
+
className: 'dsh-autorestart-settings__select',
|
|
2465
|
+
value: String(graceSeconds),
|
|
2466
|
+
disabled: busy,
|
|
2467
|
+
'data-dsh-autorestart-setting': 'restartGraceMs',
|
|
2468
|
+
onChange: (event) => {
|
|
2469
|
+
const value = Number(event && event.target ? event.target.value : NaN)
|
|
2470
|
+
if (!Number.isFinite(value) || value === graceSeconds) return
|
|
2471
|
+
void save({ restartGraceMs: value * 1000 })
|
|
2472
|
+
},
|
|
2473
|
+
},
|
|
2474
|
+
GRACE_CHOICES_SECONDS.map((choice) => h(
|
|
2475
|
+
'option',
|
|
2476
|
+
{ key: String(choice), value: String(choice) },
|
|
2477
|
+
choice === 0
|
|
2478
|
+
? t('settings.grace.immediate')
|
|
2479
|
+
: fill(t('settings.grace.value'), { n: choice }),
|
|
2480
|
+
)),
|
|
2481
|
+
),
|
|
2482
|
+
h('span', { className: 'dsh-autorestart-settings__hint' }, t('settings.grace.hint')),
|
|
2483
|
+
),
|
|
2366
2484
|
h(
|
|
2367
2485
|
'label',
|
|
2368
2486
|
{ className: 'dsh-autorestart-settings__field' },
|
|
@@ -2873,6 +2991,14 @@ if (moduleLoader) moduleLoader.load({
|
|
|
2873
2991
|
if (phase === 'waiting') {
|
|
2874
2992
|
ensureSleepStyle()
|
|
2875
2993
|
const blockers = status && Array.isArray(status.blockers) ? status.blockers : []
|
|
2994
|
+
// A pending restart is not a task: it is what the shutdown waits for,
|
|
2995
|
+
// so it is never counted as one.
|
|
2996
|
+
const tasks = blockers.filter((entry) => !isRestartPending(entry))
|
|
2997
|
+
// How many of the blockers are subagents: the card says so, so the
|
|
2998
|
+
// count is not read as "that many conversations".
|
|
2999
|
+
const subagentCount = tasks.filter(
|
|
3000
|
+
(entry) => entry && entry.kind === 'subagent',
|
|
3001
|
+
).length
|
|
2876
3002
|
return h(
|
|
2877
3003
|
'div',
|
|
2878
3004
|
{
|
|
@@ -2886,23 +3012,35 @@ if (moduleLoader) moduleLoader.load({
|
|
|
2886
3012
|
h('p', { className: 'dsh-autorestart-sleep-waiting__title' }, t('sleep.waiting.title')),
|
|
2887
3013
|
h('p', { className: 'dsh-autorestart-sleep-waiting__body' }, t('sleep.waiting.body')),
|
|
2888
3014
|
blockers.length === 0
|
|
2889
|
-
? h(
|
|
3015
|
+
? h(
|
|
3016
|
+
'p',
|
|
3017
|
+
{ className: 'dsh-autorestart-sleep-waiting__none' },
|
|
3018
|
+
// The Host's own message explains WHY the machine waits (the
|
|
3019
|
+
// grace window for work this plugin cannot see); the generic
|
|
3020
|
+
// line is only the fallback.
|
|
3021
|
+
typeof status.message === 'string' && status.message !== ''
|
|
3022
|
+
? status.message
|
|
3023
|
+
: t('sleep.waiting.none'),
|
|
3024
|
+
)
|
|
2890
3025
|
: h(
|
|
2891
3026
|
'ul',
|
|
2892
3027
|
{ className: 'dsh-autorestart-sleep-waiting__list' },
|
|
2893
3028
|
blockers.map((entry, index) => h(
|
|
2894
3029
|
'li',
|
|
2895
3030
|
{ key: 'blocker-' + index },
|
|
2896
|
-
|
|
2897
|
-
title: entry && typeof entry.title === 'string' && entry.title !== ''
|
|
2898
|
-
? entry.title
|
|
2899
|
-
: t('panelBlockedGeneric'),
|
|
2900
|
-
}),
|
|
3031
|
+
blockerLine(entry),
|
|
2901
3032
|
)),
|
|
2902
3033
|
),
|
|
2903
|
-
h('p', { className: 'dsh-autorestart-sleep-waiting__count' },
|
|
3034
|
+
h('p', { className: 'dsh-autorestart-sleep-waiting__count' }, tasks.length === 0
|
|
3035
|
+
? null
|
|
3036
|
+
: fill(t('sleep.waiting.count'), { n: tasks.length })),
|
|
3037
|
+
subagentCount === 0
|
|
2904
3038
|
? null
|
|
2905
|
-
:
|
|
3039
|
+
: h(
|
|
3040
|
+
'p',
|
|
3041
|
+
{ className: 'dsh-autorestart-sleep-waiting__subagents' },
|
|
3042
|
+
fill(t('sleep.waiting.subagents'), { k: subagentCount }),
|
|
3043
|
+
),
|
|
2906
3044
|
errorLine(),
|
|
2907
3045
|
h(
|
|
2908
3046
|
'button',
|
package/cordis.patch.yml
CHANGED
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
relaunchDelaySeconds: 2
|
|
26
26
|
# quiet window (ms) after the last conversation activity before a restart may fire
|
|
27
27
|
settleMs: 2000
|
|
28
|
+
# Grace window (ms): the extra wait after the LAST conversation finished,
|
|
29
|
+
# before the host is really restarted or switched off. This plugin can
|
|
30
|
+
# only see the DSH agents — other plugins (a memory plugin that writes
|
|
31
|
+
# its notes a few seconds later, a summary, an MCP callback) queue
|
|
32
|
+
# their work with a timer of their own, and that work is invisible
|
|
33
|
+
# here. A DROPDOWN in the settings page: 0 / 3 / 5 / 10 / 20 / 30
|
|
34
|
+
# seconds. `0` means "no extra wait" (the behaviour of 0.4.0).
|
|
35
|
+
restartGraceMs: 10000
|
|
28
36
|
# dsh web: reload the page when the host boot token changes
|
|
29
37
|
autoReloadPage: true
|
|
30
38
|
# hard-block shell commands that would kill/relaunch DSH by hand
|
package/docs/README.zh.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- 多个会话同时要求重启时合并成一次,且每个请求者都会被唤醒;
|
|
19
19
|
- 带 goal 的会话不会把重启卡死:重启前自动**解除(disarm)**它的 goal(当前那一轮照常跑完,什么都不丢),重启之后自动重新上链;
|
|
20
20
|
- 重启前有两道检查:宿主侧查一次,独立 helper 在杀进程前再查一次;
|
|
21
|
-
- 界面上能看到状态:输入框工具行左侧的"待重启 ·
|
|
21
|
+
- 界面上能看到状态:输入框工具行左侧的"待重启 · 等待 N 个任务结束"小胶囊、左侧栏会话标题旁的圆点;快重启时圆点呼吸,点胶囊看详情;
|
|
22
22
|
- 可配置:静默窗口(默认 2 秒)、杀进程前等待(默认 2 秒)、忙碌时是否允许强制重启等。
|
|
23
23
|
- 同一套"等到大家都安静"的想法还做成了**收工关机**:左侧栏最底部(设置按钮旁边)一个月亮按钮 → 等所有会话收工 → 屏幕上倒数(一张浮动卡片:大字 + 鲸鱼娘)→ **关掉整台电脑**(见「收工关机」)。
|
|
24
24
|
|
|
@@ -134,7 +134,7 @@ agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autores
|
|
|
134
134
|
|
|
135
135
|
## 安装
|
|
136
136
|
|
|
137
|
-
当前版本:**0.
|
|
137
|
+
当前版本:**0.6.0**。
|
|
138
138
|
|
|
139
139
|
```powershell
|
|
140
140
|
# 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
|
|
@@ -169,6 +169,7 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
|
|
|
169
169
|
| `restartCommand` | `''` | 自定义重启命令,支持 `{pid}` `{ppid}` `{app}` 占位符 |
|
|
170
170
|
| `relaunchDelaySeconds` | `2` | 脱离的 helper 在杀掉应用**之前**等几秒(`ping -n 3`,约 2 秒) |
|
|
171
171
|
| `settleMs` | `2000` | 静默窗口(ms):最后一次会话活动之后,宿主要安静这么久,重启才允许开始 |
|
|
172
|
+
| `restartGraceMs` | `10000` | 宽限期(ms):最后一个会话收工后,再等这么久才真正重启/关机——**下拉框:0 / 3 / 5 / 10 / 20 / 30 秒**,默认 10;`0` = 不额外等 |
|
|
172
173
|
| `autoReloadPage` | `true` | `dsh web`:host 重启(bootId 变化)后让页面自动 reload |
|
|
173
174
|
| `blockManualRestart` | `true` | 硬拦截 agent 用 shell 命令手动杀掉/重启 DSH,并引导到 `continuum_restart` |
|
|
174
175
|
| `healthPath` | `/__dsh_autorestart/health` | 健康检查路由(返回 bootId/pid/phase/自检标志) |
|
|
@@ -181,6 +182,18 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
|
|
|
181
182
|
| `sleepCountdownSeconds` | `20` | 收工关机在真正关机前倒数几秒——**下拉框:0 / 10 / 20 / 30 / 60 秒**,默认 20;`0` 表示**不倒数、立即关机**(仍会先等所有会话收工) |
|
|
182
183
|
| `sleepCommand` | `''` | 收工关机的自定义关机命令;留空 = 自动识别宿主(Windows 用 `cmd.exe /c …`,其它平台用 `/bin/sh -c …`) |
|
|
183
184
|
|
|
185
|
+
### 为什么收工后还要多等几秒
|
|
186
|
+
|
|
187
|
+
**我们只看得到 DSH 的会话。** 这个插件知道的是:某个会话还在不在跑、有没有排队的 inbox 消息、最后一个是什么时候闲下来的。它**看不到别的插件用定时器排队的活** —— 比如记忆插件在会话结束后过几秒才把这次对话写进记忆、比如一次总结、一次 MCP 回调、一次日志落盘。对我们来说这些活是不存在的,所以「所有 agent 都闲了」并不等于「现在真的没有事情在发生」。
|
|
188
|
+
|
|
189
|
+
**宽限期就是为这件事留的。** `restartGraceMs`(默认 **10 秒**)的意思是:**最后一个会话收工后,再额外等这么久,才真正重启 / 关机**。这是一层普通的、可配置的保险,**不针对任何具体插件**。
|
|
190
|
+
|
|
191
|
+
- **两条路都守它**:重启闸门等的是 `max(settleMs, restartGraceMs)` 的安静;收工关机也在**开始倒数之前**先等满这个宽限期(所以宽限期不会把已经在走的倒计时顶回去一轮)。
|
|
192
|
+
- **等待期间又有活,重新计时**:宽限期永远跟着「最后一次活动」走 —— 会话又开始干活,就从那一刻重新等。
|
|
193
|
+
- **`0` 就是「和以前一样」**:`restartGraceMs: 0` 时行为与 0.4.0 完全一致。
|
|
194
|
+
- **在哪改**:设置 → 自动重启与关机 → 「收工后再等几秒(给记忆/总结类插件留时间)」。
|
|
195
|
+
- **怎么排障**:状态接口都带着它 —— `GET /health?state=1`、`GET /health?gate=1`、`continuum_status`、`GET /sleep` 都会报 `graceMs`(配了多宽限期)和 `quietForMs`(已经安静了多久)。
|
|
196
|
+
|
|
184
197
|
## Agent 工具与"它怎么知道该用插件"
|
|
185
198
|
|
|
186
199
|
| 工具 | 作用 |
|
|
@@ -228,6 +241,45 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
|
|
|
228
241
|
|
|
229
242
|
**点一下就够了。** 月亮按钮按下就武装:宿主等所有忙会话收工,在角落里倒数 **20 秒**(浮动卡片,不遮挡界面),然后关掉整台机器。**再点一下**(或点卡片上的「算了,别关」)就取消。除此之外什么都不用用户做:没有确认框、不用点第二次。
|
|
230
243
|
|
|
244
|
+
### 关机和重启谁先谁后
|
|
245
|
+
|
|
246
|
+
**一句话版本。** 点月亮按钮的意思是「**等活干完了再关电脑**」,不是「现在就算还有别的在跑也关掉」。所以:
|
|
247
|
+
|
|
248
|
+
1. **有待重启时,关机先让路。** 队列里还有一次重启没执行时,**不会关机** —— 卡片上写的是「等这次重启完成后再关机」。先重启,会话跟着回来继续干活,等它们都收工了,机器才关。
|
|
249
|
+
2. **重启之后活还在继续。** 跟着重启回来的会话是在干活的:只要还有会话在忙,关机就一直等;**所有**会话都空了才关。
|
|
250
|
+
3. **关机的意图跨重启保留。** 你只按了**一次**按钮。重启不会把这个请求吃掉:它被写进检查点文件,新进程启动后接着等那些活 —— **你不需要再点一次**。
|
|
251
|
+
4. **随时可以反悔。** 等待卡片上有「不关了」按钮(再点一次月亮按钮也行):一按,这个意图就彻底没了。
|
|
252
|
+
|
|
253
|
+
**意图存在哪。** 检查点文件(`<DSH_HOME>/storages/dsh-autorestart/state.json`)里的 `sleep: { armed, armedAt, countdownSeconds }`。机器关掉、关机命令没跑起来、或者你点了「不关了」,它都会被清掉。**记录坏掉就直接丢弃** —— 宁可让你再点一次,也不能因为半截文件让机器永远自己关自己。
|
|
254
|
+
|
|
255
|
+
**排障用。** `GET /sleep` 会报 `armedAt`(什么时候点的关机),旁边还有 `phase`、`blockers`、`graceMs`。
|
|
256
|
+
|
|
257
|
+
### 故障排查:点了没关机
|
|
258
|
+
|
|
259
|
+
**下面是真实宿主返回的东西**(文案是中文,字段名不是):
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
POST /__dsh_autorestart/sleep {"action":"arm"} # 有 3 个会话在干活
|
|
263
|
+
→ phase=waiting armed=true remainingMs=20000(满值=没开始倒数)
|
|
264
|
+
message: 等 3 个任务收工,收工后开始倒数。
|
|
265
|
+
|
|
266
|
+
# 制造一个待执行的重启之后:
|
|
267
|
+
GET /__dsh_autorestart/sleep
|
|
268
|
+
→ phase=waiting armed=true remainingMs=20000
|
|
269
|
+
message: 等这次重启完成后再关机……
|
|
270
|
+
blockers: [{"sessionId":"restart-pending","kind":"restart-pending",
|
|
271
|
+
"label":"待执行的重启","name":"待执行的重启"}]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**`remainingMs` 是满值 = 倒数还没开始**,也就是机器在等什么东西。下面是可能的原因,以及该怎么办:
|
|
275
|
+
|
|
276
|
+
1. **还有任务在跑。** `blockers[]` 列的就是它们,看 `kind`:`session` 是会话,`subagent` 是某个会话派出去的子智能体。**子智能体也算在干活,这是故意的** —— 否则重启会把它掐断。**怎么办:** 等它干完,或者把那个会话停掉;列表会自己变短。
|
|
277
|
+
2. **有待执行的重启。** `kind` 是 `restart-pending`(`label`/`name` 都是 `待执行的重启`)。这是正常排队,不是故障:先重启,会话跟着回来,完事之后倒数自动接着走。**怎么办:** 不用管,等重启完即可(也可以用 `continuum_cancel_restart` 把这次重启取消掉)。
|
|
278
|
+
3. **刚收工,还在宽限期里**(默认 10 秒,给记忆/总结类插件留落盘时间)。**你会看到:** `graceMs` 是配的窗口,`quietForMs` 是宿主已经安静了多久。**怎么办:** 等着;等待期间又有会话开始干活,计时会重新开始(这就是这个窗口的意义)。不想要这段等待就在设置里把它调成 `0`。
|
|
279
|
+
4. **倒数期间又有会话开始干活。** 倒数退回「等待」,`remainingMs` 变回满值。**你会看到:** 消息里说又有谁开始干活了,`blockers[]` 列着是谁。**怎么办:** 等他们干完。
|
|
280
|
+
5. **想看「到底等谁」。** `continuum_status` 的 `blockedBy[]` 和 `GET /sleep` 的 `blockers[]` 是**同一份数据**(前者给 agent 看,后者给脚本看)。每条都有 `kind`(它是什么)、`label`(会话标题)、`name`(完整称呼,比如「写代码的会话的子智能体」)。标题解析不到时 `label` 是 `null`、`name` 是空字符串 —— 这时候等待那句话就只说「等 3 个任务收工」,不带名字列表。
|
|
281
|
+
6. **彻底不想关了。** 等待卡片上的取消按钮(或者再点一次月亮按钮,或者 `POST /sleep {"action":"cancel"}`)会把这个意图彻底清掉:`armed` 变回 `false`,`armedAt` 变回 `null`。
|
|
282
|
+
|
|
231
283
|
**桌面端和 web 端行为完全一致。** 没有票据、没有额外请求头、没有隐藏模式:`POST /__dsh_autorestart/sleep` 只带一个动作,两端走的是同一条路由。脚本(或者 agent)同样可以驱动它:
|
|
232
284
|
|
|
233
285
|
```bash
|
|
@@ -519,8 +571,9 @@ or by requesting the health path with ?gate=1.
|
|
|
519
571
|
|
|
520
572
|
- 胶囊渲染在**输入框工具行左侧**(`conversation.input.left` 槽位),是一个高度约 22px 的小胶囊,**不额外占一行**(不再是输入框上方的一整行)。
|
|
521
573
|
- 只有当健康路由 `/__dsh_autorestart/health?state=1` 返回 `pendingRestart: true` **且**本会话 id 出现在 `waiters` 列表里时才会出现;别的会话等待重启时你看不到它。
|
|
522
|
-
-
|
|
523
|
-
- 悬停提示(原生 `title
|
|
574
|
+
- 文案有多种状态:还有别的**任务**占着时显示「待重启 · 等待 N 个任务结束」;都空闲时显示「待重启 · 空闲后自动重启」;被某个**自动续跑的 goal** 占着时显示「待重启 · 被某个自动续跑的 goal 占着」(见下面的升级 3)。
|
|
575
|
+
- 悬停提示(原生 `title`):「等待其他任务结束(计数含"排队但还没开始"的任务)后自动重启;重启后会自动继续本会话进度」。
|
|
576
|
+
- **每个占用者都说清自己是什么**:普通会话用它自己的标题;**子智能体**用"创建它的那个会话"的标题来称呼(显示成「子智能体(那个会话)」)。**原始的 `session-xxxx` 永远不会推给界面** —— 取不到标题时用一个通用说法。
|
|
524
577
|
- 颜色走**品牌色(蓝紫)** `var(--dsw-alias-brand-primary, #5b8def)`,**不再用告警黄(warning)色**:胶囊里的小圆点和侧边栏圆点都是品牌色,胶囊边框保持中性(`--dsw-alias-border-l1`),文字保持中性。
|
|
525
578
|
- 左侧边栏会话行的**行首 16px 状态格**里也会出现一个同款 8px 圆点(`sidebar.session.row.leading` 槽位,id `autorestart-restart-dot`,order 20):纯图标,完整说明放在悬停 `title` 里,aria-label 为「待重启」/「Pending restart」。
|
|
526
579
|
- 那个 16px 状态格会被**优先级更高的行状态**(ask / 未读 / 运行中)替换,所以圆点只在该行**空闲**时出现。
|
|
@@ -572,8 +625,12 @@ or by requesting the health path with ?gate=1.
|
|
|
572
625
|
}
|
|
573
626
|
```
|
|
574
627
|
|
|
575
|
-
- `blockedBy[]` 的每一项在原有字段(`sessionId`、`reason
|
|
576
|
-
-
|
|
628
|
+
- `blockedBy[]` 的每一项在原有字段(`sessionId`、`reason`)之外,还带两个字段:
|
|
629
|
+
- `kind`:`"session"` 或 `"subagent"` —— 这个占用者是普通会话还是**子智能体**(子智能体用 `agent.origin === 'subagent'` 判定;分叉会话的 `origin` 为空,仍然是普通会话)。
|
|
630
|
+
- `label`:**它该被怎么称呼** —— 普通会话用自己的标题;子智能体用「创建它的那个会话」的标题(所以界面显示成「子智能体(那个会话)」)。取不到就是 `null`,界面改用通用说法。
|
|
631
|
+
- 另外还有 `name`(完整的称呼,如「写代码的会话的子智能体」)、`parentTitle` / `parentSessionId`(子智能体的父会话)、`title`(`label` 的旧名字,保留给老客户端)。
|
|
632
|
+
- 这些字段是**尽力而为**:会话控制器缺失、抛错或没有标题都**不会**让这条路由失败,只是 `label` 为 `null`;**原始的 `session-xxxx` 永远不会被当成名字推给界面**。
|
|
633
|
+
- 标题查询结果会缓存约 **5 秒**(客户端每 2 秒轮询一次,不会把会话列表打成风暴)。
|
|
577
634
|
- 标题查询结果会缓存约 **5 秒**(客户端每 2 秒轮询一次,不会把会话列表打成风暴)。
|
|
578
635
|
- `blockedBy[]` 里的 `reason` 可能是 **goal 相关**的:`goal-active`(持有一个会自动续跑的已武装 goal)、`goal-exhausted`(goal 的轮次额度用完,无法继续)等;此时原本的占用原因保留在 `busyReason` 里。
|
|
579
636
|
- `parkUrl` 是停泊路由的绝对路径(由配置的 `healthPath` 前缀推导),客户端优先用它,而不是自己猜路径。
|