dsh-client-auto-continue 0.5.7 → 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 +16 -9
- package/README.zh.md +15 -9
- package/lib/client.js +159 -12
- package/lib/client.js.map +2 -2
- package/lib/index.js +6 -0
- package/lib/types/client/engine.d.ts +28 -1
- package/lib/types/client/locales.d.ts +6 -0
- package/lib/types/client/settings-card.d.ts +3 -0
- package/lib/types/index.d.ts +12 -0
- package/package.json +10 -10
- package/scripts/patch-expose.mjs +0 -0
- package/src/client/engine.ts +160 -12
- package/src/client/index.ts +4 -5
- package/src/client/locales.ts +12 -0
- package/src/client/settings-card.tsx +36 -0
- package/src/index.ts +10 -0
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ For [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh we
|
|
|
40
40
|
- **Error classification** — transient failures (network / timeout / 5xx / 429…) are auto-resumed; permanent ones are **skipped** and notified, because retrying them never helps. A failure counts as permanent when its HTTP status is 401/403 or its code/message matches auth, credential/API-key, balance/quota, unknown-model, or context-length/overflow keywords. Turn classification off to resume everything
|
|
41
41
|
- **Adaptive backoff** — consecutive failures wait longer each time (cooldown × factor: 20s → 40s → 80s…), capped at the max backoff, instead of hammering a broken upstream
|
|
42
42
|
- **Templated continue text** — `continueText` supports `{code}` `{message}` `{status}` `{tool}` `{turn}` `{errorCount}` `{sessionTitle}` `{elapsed}` placeholders, so the resume message can carry the failure context ("继续 (git push failed: UPSTREAM)"); a **separate template** fires on `max-tokens` (e.g. "继续输出, 不要重复已生成的内容")
|
|
43
|
+
- **Idempotency guard** — before resuming, the plugin inspects the last tool call: if its result is unconfirmed (the turn died mid-tool, e.g. a `git push` that may have gone through), the resume message tells the model to check state first and not to rerun; if the tool is confirmed done, it says so and asks not to repeat it; a failed tool gets no guard (retrying it is the point). Both guard texts are configurable (`{tool}` / `{result}` placeholders)
|
|
43
44
|
- **Pause** — a global **Pause auto-continue** toggle in the settings card stops everything (live + scan) instantly; per-session pauses (e.g. via a notification button) suspend only one session until they expire
|
|
44
45
|
- **Notification buttons** — notifications carry **Resume now** (send immediately, ignoring cooldown and the consecutive cap) and **Pause this session 1h** actions
|
|
45
46
|
- **Stats panel** — the settings card shows today's auto-continue count, recoveries, failures, permanent skips and give-ups, broken down by error code, with a one-click reset
|
|
@@ -124,12 +125,12 @@ dsh web
|
|
|
124
125
|
|
|
125
126
|
> Switching from a manual install to `dsh plugin add`? Remove the manual `insert` entry first — the bundle patch registers the row and a duplicate would conflict.
|
|
126
127
|
|
|
127
|
-
> **
|
|
128
|
-
>
|
|
129
|
-
>
|
|
130
|
-
>
|
|
131
|
-
>
|
|
132
|
-
>
|
|
128
|
+
> **Note (DSH ≥ 0.1.0-rc.7):** upstream made settings exposure **registry-driven**
|
|
129
|
+
> — every namespace a plugin registers is served to the web settings surface,
|
|
130
|
+
> so the settings card just works, no vendor patch needed. Only DSH 0.1.0-rc.6
|
|
131
|
+
> and earlier gate exposure behind a hardcoded allowlist in
|
|
132
|
+
> `@deepseek-ai/dsh-host-apiproxy`; on those versions, run the idempotent
|
|
133
|
+
> vendor patch once (and re-run after reinstalling dsh):
|
|
133
134
|
>
|
|
134
135
|
> ```sh
|
|
135
136
|
> npx --yes --package dsh-client-auto-continue patch-expose
|
|
@@ -143,7 +144,7 @@ dsh web
|
|
|
143
144
|
> own. If you installed from this repository instead, the same script is
|
|
144
145
|
> `node scripts/patch-expose.mjs` at the repo root. The auto-continue engine
|
|
145
146
|
> itself works without this patch — it only gates the GUI settings section;
|
|
146
|
-
>
|
|
147
|
+
> on rc.6 and earlier, configure everything directly in `~/.dsh/settings.yaml`
|
|
147
148
|
> (see [Configuration](#configuration)).
|
|
148
149
|
|
|
149
150
|
### Verify & uninstall
|
|
@@ -173,6 +174,9 @@ auto-continue:
|
|
|
173
174
|
paused: false
|
|
174
175
|
continueText: '继续'
|
|
175
176
|
continueTextMaxTokens: '继续'
|
|
177
|
+
guardTools: true
|
|
178
|
+
guardPendingText: '(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)'
|
|
179
|
+
guardDoneText: '(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)'
|
|
176
180
|
graceMs: 3000
|
|
177
181
|
cooldownMs: 20000
|
|
178
182
|
maxConsecutive: 3
|
|
@@ -204,6 +208,9 @@ auto-continue:
|
|
|
204
208
|
| Pause auto-continue | `off` | Global pause: no live or scan auto-send fires, queued pending sends are cancelled |
|
|
205
209
|
| Continue text | `继续` | Text automatically sent after an interruption |
|
|
206
210
|
| Continue text (max tokens) | `继续` | Text sent when the output token ceiling is reached (same placeholders) |
|
|
211
|
+
| Idempotency guard | `on` | Inspect the last tool call before resuming and steer the model (see What It Does) |
|
|
212
|
+
| Guard text (unconfirmed result) | `(上一步工具「{tool}」可能未完成…)` | Appended when the last tool may have partially executed; `{tool}` placeholder |
|
|
213
|
+
| Guard text (tool succeeded) | `(上一步工具「{tool}」已完成…)` | Appended when the last tool is confirmed done; `{tool}` / `{result}` placeholders |
|
|
207
214
|
| Grace period (ms) | `3000` | Wait after an interruption; cancelled if the host recovers on its own |
|
|
208
215
|
| Cooldown (ms) | `20000` | Min interval between auto-continues per session (failed attempts count too) |
|
|
209
216
|
| Max consecutive | `3` | Max consecutive auto-continues; stops until a user intervenes or a turn completes |
|
|
@@ -218,7 +225,7 @@ auto-continue:
|
|
|
218
225
|
| Max backoff (ms) | `300000` | Cap on the adaptive backoff interval |
|
|
219
226
|
| Browser notifications | `off` | Notify when auto-continue fires, gives up, or hits a permanent error |
|
|
220
227
|
|
|
221
|
-
`continueText` (and `continueTextMaxTokens`) accept the placeholders `{code}`, `{message}`, `{status}`, `{tool}` (last tool call before the failure), `{turn}`, `{errorCount}` (consecutive failures including this one), `{sessionTitle}` (from the session list) and `{elapsed}` (time since the failure, e.g. `1m5s`) — e.g. `继续 ({tool}: {code})` becomes `继续 (git push: UPSTREAM)`.
|
|
228
|
+
`continueText` (and `continueTextMaxTokens`) accept the placeholders `{code}`, `{message}`, `{status}`, `{tool}` (last tool call before the failure), `{turn}`, `{errorCount}` (consecutive failures including this one), `{sessionTitle}` (from the session list) and `{elapsed}` (time since the failure, e.g. `1m5s`) — e.g. `继续 ({tool}: {code})` becomes `继续 (git push: UPSTREAM)`. The guard texts accept `{tool}` and `{result}` (a truncated excerpt of the last tool output).
|
|
222
229
|
|
|
223
230
|
---
|
|
224
231
|
|
|
@@ -239,7 +246,7 @@ The plugin is browser-only and touches **no files, credentials, or network beyon
|
|
|
239
246
|
npm run typecheck # tsc --noEmit
|
|
240
247
|
npm run build # lib/client.js + lib/index.js + lib/types
|
|
241
248
|
npm run watch # rebuild on change; host HMR hot-reloads without a page refresh
|
|
242
|
-
npm run test # node tests/simulate.mjs —
|
|
249
|
+
npm run test # node tests/simulate.mjs — 27 behavioral scenarios
|
|
243
250
|
```
|
|
244
251
|
|
|
245
252
|
While `npm run watch` runs, the profile's client-hmr row polls `lib/client.js` every 500 ms and hot-reloads the plugin in the browser — no server restart needed for code changes.
|
package/README.zh.md
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
- **错误分类** — 临时性错误(网络 / 超时 / 5xx / 429 等)自动续跑; 永久性错误跳过并通知, 因为重试也没用。判定为永久性的条件: HTTP 状态码 401/403, 或 code/message 命中认证、凭据/API Key、余额/配额、模型不存在、上下文长度/超限等关键词。关闭分类后则全部自动继续
|
|
41
41
|
- **自适应退避** — 连续失败时等待时间递增(冷却 × 系数: 20s → 40s → 80s…), 有上限, 不再对故障上游狂轰滥炸
|
|
42
42
|
- **模板化继续文本** — `continueText` 支持 `{code}` `{message}` `{status}` `{tool}` `{turn}` `{errorCount}` `{sessionTitle}` `{elapsed}` 占位符, 续跑消息可携带失败上下文(如「继续 (git push 失败: UPSTREAM)」); 达到 `max-tokens` 时使用**另一套模板**(如「继续输出, 不要重复已生成的内容」)
|
|
43
|
+
- **幂等护栏** — 续跑前检查上一步工具调用: 结果未确认(回合在工具执行中途夭折, 如 `git push` 可能已经推上去了)时, 续跑消息会提示模型先确认状态、不要重复执行; 工具已确认成功时说明已完成、请勿重复; 工具失败则不加护栏(重试本来就是目的)。两段护栏文本都可配置(支持 `{tool}` / `{result}` 占位符)
|
|
43
44
|
- **暂停** — 设置卡片里的全局 **暂停自动继续** 开关可立即停掉一切(实时 + 扫描); 会话级暂停(如通过通知按钮)只挂起单个会话, 到期自动恢复
|
|
44
45
|
- **通知按钮** — 通知带 **立即续跑**(无视冷却与连续上限, 马上发送)和 **暂停该会话 1 小时** 按钮
|
|
45
46
|
- **统计面板** — 设置卡片展示今日自动继续次数、恢复成功、继续后失败、永久性跳过、达上限停止, 按错误码分布, 可一键清零
|
|
@@ -124,11 +125,10 @@ dsh web
|
|
|
124
125
|
|
|
125
126
|
> 从手动安装切换到 `dsh plugin add` 时, 请先删掉手动加的 `insert` 条目——包自带的 bundle patch 会注册插件行, 重复注册会冲突。
|
|
126
127
|
|
|
127
|
-
>
|
|
128
|
-
>
|
|
129
|
-
>
|
|
130
|
-
>
|
|
131
|
-
> 没有任何插件相关的硬编码字符串。执行一次即可, 重新安装 dsh 后重跑:
|
|
128
|
+
> **说明(DSH ≥ 0.1.0-rc.7):** 上游已把设置暴露改成**注册表驱动**——插件注册的
|
|
129
|
+
> 任何命名空间都会直接出现在 webui 设置区, 设置卡片开箱即用, 无需任何供应商补丁。
|
|
130
|
+
> 仅 DSH 0.1.0-rc.6 及更早版本仍在 `@deepseek-ai/dsh-host-apiproxy` 里用硬编码白名单
|
|
131
|
+
> 门禁暴露; 这些版本需要执行一次幂等的供应商补丁(重新安装 dsh 后重跑):
|
|
132
132
|
>
|
|
133
133
|
> ```sh
|
|
134
134
|
> npx --yes --package dsh-client-auto-continue patch-expose
|
|
@@ -139,8 +139,8 @@ dsh web
|
|
|
139
139
|
> 无需猜测 profile 路径), 并覆盖所有可达的 dsh 安装: profile 链接副本、全局
|
|
140
140
|
> `npm i -g @deepseek-ai/dsh` 安装、以及当前目录的安装。若是从本仓库安装,
|
|
141
141
|
> 同一脚本在仓库根目录执行 `node scripts/patch-expose.mjs` 即可。自动续跑引擎
|
|
142
|
-
> 本身不依赖这个补丁——它只影响 GUI 设置区是否可见;
|
|
143
|
-
> `~/.dsh/settings.yaml` 里配置全部参数(见 [配置](#配置))。
|
|
142
|
+
> 本身不依赖这个补丁——它只影响 GUI 设置区是否可见; 在 rc.6 及更早版本上,
|
|
143
|
+
> 可以直接在 `~/.dsh/settings.yaml` 里配置全部参数(见 [配置](#配置))。
|
|
144
144
|
|
|
145
145
|
### 验证与卸载
|
|
146
146
|
|
|
@@ -169,6 +169,9 @@ auto-continue:
|
|
|
169
169
|
paused: false
|
|
170
170
|
continueText: '继续'
|
|
171
171
|
continueTextMaxTokens: '继续'
|
|
172
|
+
guardTools: true
|
|
173
|
+
guardPendingText: '(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)'
|
|
174
|
+
guardDoneText: '(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)'
|
|
172
175
|
graceMs: 3000
|
|
173
176
|
cooldownMs: 20000
|
|
174
177
|
maxConsecutive: 3
|
|
@@ -200,6 +203,9 @@ auto-continue:
|
|
|
200
203
|
| 暂停自动继续 | 关 | 全局暂停: 实时与扫描都不再自动发送, 已排队的待发送也会取消 |
|
|
201
204
|
| 继续文本 | `继续` | 中断后自动发送的消息内容 |
|
|
202
205
|
| 超限时的继续文本 | `继续` | 达到输出 token 上限时自动发送的文本(支持相同占位符) |
|
|
206
|
+
| 幂等护栏 | 开 | 续跑前检查上一步工具调用并给出指引(见「它做什么」) |
|
|
207
|
+
| 结果未确认时的护栏文本 | `(上一步工具「{tool}」可能未完成…)` | 上一步工具可能已部分执行时附加; 支持 {tool} 占位符 |
|
|
208
|
+
| 工具已成功时的护栏文本 | `(上一步工具「{tool}」已完成…)` | 上一步工具已确认成功时附加; 支持 {tool} / {result} 占位符 |
|
|
203
209
|
| 宽限期 (ms) | `3000` | 中断后等待的时长; 期间宿主自行恢复则取消 |
|
|
204
210
|
| 冷却时间 (ms) | `20000` | 同一会话两次自动「继续」的最小间隔(失败尝试也计入) |
|
|
205
211
|
| 最大连续次数 | `3` | 同一会话连续自动「继续」上限; 超过后停止, 直到用户介入或成功回合 |
|
|
@@ -214,7 +220,7 @@ auto-continue:
|
|
|
214
220
|
| 最大退避间隔 (ms) | `300000` | 自适应退避的上限 |
|
|
215
221
|
| 浏览器通知 | 关 | 自动继续成功 / 放弃 / 遇到永久性错误时弹通知 |
|
|
216
222
|
|
|
217
|
-
`continueText`(以及 `continueTextMaxTokens`)支持占位符 `{code}`、`{message}`、`{status}`、`{tool}`(失败前最后一次工具调用)、`{turn}`、`{errorCount}`(连续失败次数, 含本次)、`{sessionTitle}`(来自会话列表)和 `{elapsed}`(距失败经过的时间, 如 `1m5s`)——例如 `继续 ({tool}: {code})` 会变成 `继续 (git push: UPSTREAM)
|
|
223
|
+
`continueText`(以及 `continueTextMaxTokens`)支持占位符 `{code}`、`{message}`、`{status}`、`{tool}`(失败前最后一次工具调用)、`{turn}`、`{errorCount}`(连续失败次数, 含本次)、`{sessionTitle}`(来自会话列表)和 `{elapsed}`(距失败经过的时间, 如 `1m5s`)——例如 `继续 ({tool}: {code})` 会变成 `继续 (git push: UPSTREAM)`。护栏文本支持 `{tool}` 与 `{result}`(上一步工具输出的截断摘要)。
|
|
218
224
|
|
|
219
225
|
---
|
|
220
226
|
|
|
@@ -235,7 +241,7 @@ auto-continue:
|
|
|
235
241
|
npm run typecheck # tsc --noEmit
|
|
236
242
|
npm run build # lib/client.js + lib/index.js + lib/types
|
|
237
243
|
npm run watch # 监听变更自动重建; 宿主 HMR 免刷新热重载
|
|
238
|
-
npm run test # node tests/simulate.mjs —
|
|
244
|
+
npm run test # node tests/simulate.mjs — 27 个行为场景
|
|
239
245
|
```
|
|
240
246
|
|
|
241
247
|
`npm run watch` 运行时, profile 的 client-hmr 行每 500ms 轮询 `lib/client.js` 并在浏览器中热重载插件——改代码无需重启服务。
|
package/lib/client.js
CHANGED
|
@@ -42,6 +42,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
42
42
|
var DEFAULT_CONFIG = {
|
|
43
43
|
continueText: "继续",
|
|
44
44
|
continueTextMaxTokens: "继续",
|
|
45
|
+
guardTools: true,
|
|
46
|
+
guardPendingText: "(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)",
|
|
47
|
+
guardDoneText: "(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)",
|
|
45
48
|
graceMs: 3e3,
|
|
46
49
|
cooldownMs: 2e4,
|
|
47
50
|
maxConsecutive: 3,
|
|
@@ -67,9 +70,14 @@ function resolveConfig(section) {
|
|
|
67
70
|
const value = section ?? {};
|
|
68
71
|
const text = typeof value.continueText === "string" && value.continueText.trim() !== "" ? value.continueText : DEFAULT_CONFIG.continueText;
|
|
69
72
|
const maxTokensText = typeof value.continueTextMaxTokens === "string" && value.continueTextMaxTokens.trim() !== "" ? value.continueTextMaxTokens : DEFAULT_CONFIG.continueTextMaxTokens;
|
|
73
|
+
const guardPendingText = typeof value.guardPendingText === "string" && value.guardPendingText.trim() !== "" ? value.guardPendingText : DEFAULT_CONFIG.guardPendingText;
|
|
74
|
+
const guardDoneText = typeof value.guardDoneText === "string" && value.guardDoneText.trim() !== "" ? value.guardDoneText : DEFAULT_CONFIG.guardDoneText;
|
|
70
75
|
return {
|
|
71
76
|
continueText: text,
|
|
72
77
|
continueTextMaxTokens: maxTokensText,
|
|
78
|
+
guardTools: booleanOr(value.guardTools, DEFAULT_CONFIG.guardTools),
|
|
79
|
+
guardPendingText,
|
|
80
|
+
guardDoneText,
|
|
73
81
|
graceMs: numberOr(value.graceMs, DEFAULT_CONFIG.graceMs),
|
|
74
82
|
cooldownMs: numberOr(value.cooldownMs, DEFAULT_CONFIG.cooldownMs),
|
|
75
83
|
maxConsecutive: Math.max(1, numberOr(value.maxConsecutive, DEFAULT_CONFIG.maxConsecutive)),
|
|
@@ -139,7 +147,31 @@ function formatElapsed(ms) {
|
|
|
139
147
|
return `${Math.floor(s / 60)}m${s % 60 > 0 ? `${s % 60}s` : ""}`;
|
|
140
148
|
}
|
|
141
149
|
function fillTemplate(template, ctx) {
|
|
142
|
-
return template.replace(/\{code\}/g, ctx.facts?.code ?? "").replace(/\{message\}/g, ctx.facts?.message ?? "").replace(/\{status\}/g, ctx.facts?.status !== void 0 ? String(ctx.facts.status) : "").replace(/\{tool\}/g, ctx.tool ?? "").replace(/\{turn\}/g, ctx.turn !== void 0 ? String(ctx.turn) : "").replace(/\{errorCount\}/g, ctx.errorCount !== void 0 ? String(ctx.errorCount) : "").replace(/\{sessionTitle\}/g, ctx.sessionTitle ?? "").replace(/\{elapsed\}/g, formatElapsed(ctx.elapsedMs));
|
|
150
|
+
return template.replace(/\{code\}/g, ctx.facts?.code ?? "").replace(/\{message\}/g, ctx.facts?.message ?? "").replace(/\{status\}/g, ctx.facts?.status !== void 0 ? String(ctx.facts.status) : "").replace(/\{tool\}/g, ctx.tool ?? "").replace(/\{turn\}/g, ctx.turn !== void 0 ? String(ctx.turn) : "").replace(/\{errorCount\}/g, ctx.errorCount !== void 0 ? String(ctx.errorCount) : "").replace(/\{sessionTitle\}/g, ctx.sessionTitle ?? "").replace(/\{elapsed\}/g, formatElapsed(ctx.elapsedMs)).replace(/\{result\}/g, ctx.result ?? "");
|
|
151
|
+
}
|
|
152
|
+
var TOOL_RESULT_CAP = 160;
|
|
153
|
+
function extractText(blocks, cap) {
|
|
154
|
+
let out = "";
|
|
155
|
+
const walk = (value) => {
|
|
156
|
+
if (out.length >= cap) return;
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
for (const item of value) walk(item);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (typeof value !== "object" || value === null) return;
|
|
162
|
+
const record = value;
|
|
163
|
+
if (record["type"] === "text" && typeof record["text"] === "string") {
|
|
164
|
+
out += record["text"];
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const child of Object.values(record)) walk(child);
|
|
168
|
+
};
|
|
169
|
+
walk(blocks);
|
|
170
|
+
return out.slice(0, cap);
|
|
171
|
+
}
|
|
172
|
+
function toolResultFacts(data) {
|
|
173
|
+
const failed = data.error !== void 0 || data.message?.content?.[0]?.isError === true;
|
|
174
|
+
return { ok: !failed, excerpt: extractText(data.message?.content?.[0]?.content, TOOL_RESULT_CAP) };
|
|
143
175
|
}
|
|
144
176
|
function effectiveCooldown(consecutive, base, factor, max) {
|
|
145
177
|
const multiplier = Math.pow(factor, consecutive);
|
|
@@ -284,6 +316,7 @@ var freshState = () => ({
|
|
|
284
316
|
lastFailure: void 0,
|
|
285
317
|
lastFailureAt: 0,
|
|
286
318
|
lastTool: void 0,
|
|
319
|
+
lastToolResult: void 0,
|
|
287
320
|
lastTurn: void 0,
|
|
288
321
|
pendingRecoveryAt: 0
|
|
289
322
|
});
|
|
@@ -392,7 +425,16 @@ var AutoContinueRunner = class {
|
|
|
392
425
|
case "session/event":
|
|
393
426
|
if (frame.event.type === "tool/call") {
|
|
394
427
|
const name = frame.event.data.name;
|
|
395
|
-
if (typeof name === "string")
|
|
428
|
+
if (typeof name === "string") {
|
|
429
|
+
const state = this.state(frame.sessionId);
|
|
430
|
+
state.lastTool = name;
|
|
431
|
+
state.lastToolResult = "pending";
|
|
432
|
+
}
|
|
433
|
+
} else if (frame.event.type === "tool/result") {
|
|
434
|
+
const state = this.state(frame.sessionId);
|
|
435
|
+
if (state.lastToolResult === "pending") {
|
|
436
|
+
state.lastToolResult = toolResultFacts(frame.event.data);
|
|
437
|
+
}
|
|
396
438
|
}
|
|
397
439
|
this.onSessionEvent(frame.sessionId, frame.event);
|
|
398
440
|
break;
|
|
@@ -412,6 +454,8 @@ var AutoContinueRunner = class {
|
|
|
412
454
|
switch (event.type) {
|
|
413
455
|
case "turn/start":
|
|
414
456
|
state.running = true;
|
|
457
|
+
state.lastTool = void 0;
|
|
458
|
+
state.lastToolResult = void 0;
|
|
415
459
|
this.cancelPending(sessionId, "宿主自行开启新回合");
|
|
416
460
|
break;
|
|
417
461
|
case "turn/end": {
|
|
@@ -639,14 +683,7 @@ var AutoContinueRunner = class {
|
|
|
639
683
|
sessionTitle = info?.title;
|
|
640
684
|
}
|
|
641
685
|
}
|
|
642
|
-
const text =
|
|
643
|
-
facts: state.lastFailure,
|
|
644
|
-
tool: state.lastTool,
|
|
645
|
-
turn: state.lastTurn,
|
|
646
|
-
errorCount: state.consecutive + 1,
|
|
647
|
-
sessionTitle,
|
|
648
|
-
elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : void 0
|
|
649
|
-
});
|
|
686
|
+
const text = this.buildContinueText(config, state, template, sessionTitle);
|
|
650
687
|
const zone = clientTimeZone();
|
|
651
688
|
state.lastAttemptAt = Date.now();
|
|
652
689
|
try {
|
|
@@ -694,6 +731,40 @@ var AutoContinueRunner = class {
|
|
|
694
731
|
releaseSend(sessionId);
|
|
695
732
|
}
|
|
696
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* 组装本次续跑消息: 模板填充 + 幂等护栏。
|
|
736
|
+
* 护栏依据上一步工具调用的执行状态附加指引, 防止重跑副作用操作:
|
|
737
|
+
* - 结果未确认(可能已部分执行)→ 提示先确认状态、不要重复执行
|
|
738
|
+
* - 已确认成功 → 提示已完成、不要重复执行
|
|
739
|
+
* - 已失败 → 不加护栏(重试工具本来就是目的)
|
|
740
|
+
*/
|
|
741
|
+
buildContinueText(config, state, template, sessionTitle) {
|
|
742
|
+
let text = fillTemplate(template, {
|
|
743
|
+
facts: state.lastFailure,
|
|
744
|
+
tool: state.lastTool,
|
|
745
|
+
turn: state.lastTurn,
|
|
746
|
+
errorCount: state.consecutive + 1,
|
|
747
|
+
sessionTitle,
|
|
748
|
+
elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : void 0
|
|
749
|
+
});
|
|
750
|
+
if (!config.guardTools) return text;
|
|
751
|
+
const guard = this.currentGuard(state);
|
|
752
|
+
if (guard.kind === "pending") {
|
|
753
|
+
text += ` ${fillTemplate(config.guardPendingText, { tool: guard.tool, result: guard.result })}`;
|
|
754
|
+
} else if (guard.kind === "done") {
|
|
755
|
+
text += ` ${fillTemplate(config.guardDoneText, { tool: guard.tool, result: guard.result })}`;
|
|
756
|
+
}
|
|
757
|
+
return text;
|
|
758
|
+
}
|
|
759
|
+
/** 上一步工具调用的护栏状态(实时路径, 由 mux 帧维护)。 */
|
|
760
|
+
currentGuard(state) {
|
|
761
|
+
if (state.lastTool === void 0 || state.lastToolResult === void 0) return { kind: "none" };
|
|
762
|
+
if (state.lastToolResult === "pending") return { kind: "pending", tool: state.lastTool };
|
|
763
|
+
if (state.lastToolResult.ok) {
|
|
764
|
+
return { kind: "done", tool: state.lastTool, result: state.lastToolResult.excerpt };
|
|
765
|
+
}
|
|
766
|
+
return { kind: "failed", tool: state.lastTool };
|
|
767
|
+
}
|
|
697
768
|
/** 查一次 session.list, 顺带缓存该会话的标题。 */
|
|
698
769
|
async fetchSessionInfo(sessionId) {
|
|
699
770
|
try {
|
|
@@ -797,11 +868,34 @@ var AutoContinueRunner = class {
|
|
|
797
868
|
if (superseded) break;
|
|
798
869
|
}
|
|
799
870
|
if (superseded) continue;
|
|
871
|
+
this.applyGuardFromEvents(state, events, lastEnd.seq);
|
|
800
872
|
this.log(`扫描发现中断 ${summary.sessionId}(turn/end:${reason.kind}), 安排自动继续`);
|
|
801
873
|
this.schedule(summary.sessionId, `scan:turn/end:${reason.kind}`);
|
|
802
874
|
}
|
|
803
875
|
return true;
|
|
804
876
|
}
|
|
877
|
+
/** 从历史事件恢复上一步工具调用状态(扫描路径的幂等护栏)。 */
|
|
878
|
+
applyGuardFromEvents(state, events, untilSeq) {
|
|
879
|
+
state.lastTool = void 0;
|
|
880
|
+
state.lastToolResult = void 0;
|
|
881
|
+
let call;
|
|
882
|
+
for (const entry of events) {
|
|
883
|
+
const event = entry.event;
|
|
884
|
+
if (event.seq >= untilSeq) continue;
|
|
885
|
+
if (event.type === "tool/call") call = event;
|
|
886
|
+
}
|
|
887
|
+
if (call === void 0) return;
|
|
888
|
+
state.lastTool = call.data.name;
|
|
889
|
+
state.lastToolResult = "pending";
|
|
890
|
+
for (const entry of events) {
|
|
891
|
+
const event = entry.event;
|
|
892
|
+
if (event.seq <= call.seq || event.seq >= untilSeq) continue;
|
|
893
|
+
if (event.type === "tool/result") {
|
|
894
|
+
state.lastToolResult = toolResultFacts(event.data);
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
805
899
|
};
|
|
806
900
|
|
|
807
901
|
// src/client/locales.ts
|
|
@@ -814,6 +908,12 @@ var zh = {
|
|
|
814
908
|
"field.continueTextHint": "中断后自动发送的消息内容。",
|
|
815
909
|
"field.continueTextMaxTokens": "超限时的继续文本",
|
|
816
910
|
"field.continueTextMaxTokensHint": "达到输出 token 上限时自动发送的文本, 支持与继续文本相同的占位符。",
|
|
911
|
+
"field.guardTools": "幂等护栏",
|
|
912
|
+
"field.guardToolsHint": "续跑前检查上一步工具调用: 结果未确认时提示先确认状态, 已成功时提示不要重复执行, 避免重复 commit/调 API。",
|
|
913
|
+
"field.guardPendingText": "结果未确认时的护栏文本",
|
|
914
|
+
"field.guardPendingTextHint": "上一步工具可能已部分执行时附加到继续文本之后, 支持 {tool} 占位符。",
|
|
915
|
+
"field.guardDoneText": "工具已成功时的护栏文本",
|
|
916
|
+
"field.guardDoneTextHint": "上一步工具已确认成功时附加到继续文本之后, 支持 {tool} 与 {result}(结果摘要)占位符。",
|
|
817
917
|
"field.graceMs": "宽限期 (ms)",
|
|
818
918
|
"field.graceMsHint": "检测到中断后等待的时长; 期间宿主自行恢复则取消。",
|
|
819
919
|
"field.cooldownMs": "冷却时间 (ms)",
|
|
@@ -878,6 +978,12 @@ var en = {
|
|
|
878
978
|
"field.continueTextHint": "Message automatically sent after an interruption.",
|
|
879
979
|
"field.continueTextMaxTokens": "Continue text (max tokens)",
|
|
880
980
|
"field.continueTextMaxTokensHint": "Text sent when the output token ceiling is reached; same placeholders as the continue text.",
|
|
981
|
+
"field.guardTools": "Idempotency guard",
|
|
982
|
+
"field.guardToolsHint": "Before resuming, inspect the last tool call: if its result is unconfirmed, tell the model to check state first; if it succeeded, tell it not to rerun — avoids duplicate commits / API calls.",
|
|
983
|
+
"field.guardPendingText": "Guard text (unconfirmed result)",
|
|
984
|
+
"field.guardPendingTextHint": "Appended when the last tool may have partially executed; supports the {tool} placeholder.",
|
|
985
|
+
"field.guardDoneText": "Guard text (tool succeeded)",
|
|
986
|
+
"field.guardDoneTextHint": "Appended when the last tool is confirmed done; supports {tool} and {result} (result excerpt).",
|
|
881
987
|
"field.graceMs": "Grace period (ms)",
|
|
882
988
|
"field.graceMsHint": "Wait after an interruption; cancelled if the host recovers on its own.",
|
|
883
989
|
"field.cooldownMs": "Cooldown (ms)",
|
|
@@ -1299,6 +1405,9 @@ var AutoContinueSettingsCardController = class {
|
|
|
1299
1405
|
booleanField("paused"),
|
|
1300
1406
|
textField("continueText"),
|
|
1301
1407
|
textField("continueTextMaxTokens"),
|
|
1408
|
+
booleanField("guardTools"),
|
|
1409
|
+
textField("guardPendingText"),
|
|
1410
|
+
textField("guardDoneText"),
|
|
1302
1411
|
numberField("graceMs", 0),
|
|
1303
1412
|
numberField("cooldownMs", 0),
|
|
1304
1413
|
numberField("maxConsecutive", 1),
|
|
@@ -1321,6 +1430,9 @@ var AutoContinueSettingsCardController = class {
|
|
|
1321
1430
|
paused: this.form.field("paused"),
|
|
1322
1431
|
continueText: this.form.field("continueText"),
|
|
1323
1432
|
continueTextMaxTokens: this.form.field("continueTextMaxTokens"),
|
|
1433
|
+
guardTools: this.form.field("guardTools"),
|
|
1434
|
+
guardPendingText: this.form.field("guardPendingText"),
|
|
1435
|
+
guardDoneText: this.form.field("guardDoneText"),
|
|
1324
1436
|
graceMs: this.form.field("graceMs"),
|
|
1325
1437
|
cooldownMs: this.form.field("cooldownMs"),
|
|
1326
1438
|
maxConsecutive: this.form.field("maxConsecutive"),
|
|
@@ -1600,6 +1712,42 @@ function AutoContinueSettingsCard(props) {
|
|
|
1600
1712
|
onReset: () => props.resetField("continueTextMaxTokens")
|
|
1601
1713
|
}
|
|
1602
1714
|
),
|
|
1715
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1716
|
+
BooleanField,
|
|
1717
|
+
{
|
|
1718
|
+
id: "auto-continue-guard-tools",
|
|
1719
|
+
label: t("field.guardTools"),
|
|
1720
|
+
hint: t("field.guardToolsHint"),
|
|
1721
|
+
...shared,
|
|
1722
|
+
...state.guardTools,
|
|
1723
|
+
onEdit: (text) => props.edit("guardTools", text),
|
|
1724
|
+
onReset: () => props.resetField("guardTools")
|
|
1725
|
+
}
|
|
1726
|
+
),
|
|
1727
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1728
|
+
ValueField,
|
|
1729
|
+
{
|
|
1730
|
+
id: "auto-continue-guard-pending-text",
|
|
1731
|
+
label: t("field.guardPendingText"),
|
|
1732
|
+
hint: t("field.guardPendingTextHint"),
|
|
1733
|
+
...shared,
|
|
1734
|
+
...state.guardPendingText,
|
|
1735
|
+
onEdit: (text) => props.edit("guardPendingText", text),
|
|
1736
|
+
onReset: () => props.resetField("guardPendingText")
|
|
1737
|
+
}
|
|
1738
|
+
),
|
|
1739
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1740
|
+
ValueField,
|
|
1741
|
+
{
|
|
1742
|
+
id: "auto-continue-guard-done-text",
|
|
1743
|
+
label: t("field.guardDoneText"),
|
|
1744
|
+
hint: t("field.guardDoneTextHint"),
|
|
1745
|
+
...shared,
|
|
1746
|
+
...state.guardDoneText,
|
|
1747
|
+
onEdit: (text) => props.edit("guardDoneText", text),
|
|
1748
|
+
onReset: () => props.resetField("guardDoneText")
|
|
1749
|
+
}
|
|
1750
|
+
),
|
|
1603
1751
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1604
1752
|
ValueField,
|
|
1605
1753
|
{
|
|
@@ -1787,8 +1935,7 @@ function apply(ctx) {
|
|
|
1787
1935
|
() => ctx.slots.register(
|
|
1788
1936
|
{
|
|
1789
1937
|
name: "settings.plugin.item",
|
|
1790
|
-
|
|
1791
|
-
order: 90,
|
|
1938
|
+
key: SETTINGS_NS,
|
|
1792
1939
|
locale: NS,
|
|
1793
1940
|
inject: () => controller.inject()
|
|
1794
1941
|
},
|