dsh-client-auto-continue 0.6.3 → 0.7.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 +15 -4
- package/README.zh.md +15 -4
- package/lib/client.js +215 -12
- package/lib/client.js.map +2 -2
- package/lib/index.js +11 -1
- package/lib/types/client/engine.d.ts +27 -0
- package/lib/types/client/locales.d.ts +11 -0
- package/lib/types/client/settings-card.d.ts +5 -0
- package/lib/types/index.d.ts +20 -0
- package/package.json +1 -1
- package/src/client/engine.ts +154 -7
- package/src/client/locales.ts +22 -0
- package/src/client/settings-card.tsx +71 -1
- package/src/index.ts +12 -0
package/README.md
CHANGED
|
@@ -43,7 +43,8 @@ For [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh we
|
|
|
43
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)
|
|
44
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. The **Resume now** notification button is the one explicit exception: pressing it is the user asking for exactly one send, pause or not
|
|
45
45
|
- **Notification buttons** — notifications carry **Resume now** (send immediately, ignoring cooldown, the consecutive cap and any pause) and **Pause this session 1h** actions
|
|
46
|
-
- **
|
|
46
|
+
- **Loop guard** — watches **running** turns too: many consecutive short model messages with no tool call in between (the "Let me read…" spin), or the same tool called repeatedly, trips the guard, which cancels the turn and restarts it with a configurable loop text ("stop repeating, try another way"). The cancel carries an internal marker so it is never confused with a user stop — the restart only happens for guard-initiated cancels. Thresholds and the loop text are configurable
|
|
47
|
+
- **Stats panel** — the settings card shows today's auto-continue count, recoveries, failures, permanent skips, give-ups and loop breaks, broken down by error code, with a one-click reset
|
|
47
48
|
- **Browser notifications** — optional alerts when auto-continue fires, gives up, or hits a permanent error; the browser asks for permission on first use, and nothing is shown again after a denial
|
|
48
49
|
|
|
49
50
|
It watches the live event streams and reacts to:
|
|
@@ -172,6 +173,11 @@ auto-continue:
|
|
|
172
173
|
backoffFactor: 2
|
|
173
174
|
backoffMaxMs: 300000
|
|
174
175
|
notify: false
|
|
176
|
+
loopGuard: true
|
|
177
|
+
loopShortChars: 40
|
|
178
|
+
loopShortCount: 8
|
|
179
|
+
loopToolRepeat: 4
|
|
180
|
+
loopText: '(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)'
|
|
175
181
|
```
|
|
176
182
|
|
|
177
183
|
**How the card works:**
|
|
@@ -191,8 +197,13 @@ auto-continue:
|
|
|
191
197
|
| Continue text | `继续` | Text automatically sent after an interruption |
|
|
192
198
|
| Continue text (max tokens) | `继续` | Text sent when the output token ceiling is reached (same placeholders) |
|
|
193
199
|
| Idempotency guard | `on` | Inspect the last tool call before resuming and steer the model (see What It Does) |
|
|
194
|
-
|
|
|
195
|
-
|
|
|
200
|
+
| Loop guard | `on` | Detect a running turn spinning in place and restart it (see What It Does) |
|
|
201
|
+
| Short-sentence max (chars) | `40` | A model message shorter than this counts as a short sentence (spinning signal) |
|
|
202
|
+
| Short-sentence threshold | `8` | Consecutive short sentences with no tool call in between trip the loop guard |
|
|
203
|
+
| Same-tool repeat count | `4` | Consecutive identical tool calls trip the loop guard |
|
|
204
|
+
| Loop text | `(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)` | Text sent after the loop guard restarts a turn; `{tool}` placeholder |
|
|
205
|
+
| Guard text (unconfirmed result) | `(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)` | Appended when the last tool may have partially executed; `{tool}` placeholder |
|
|
206
|
+
| Guard text (tool succeeded) | `(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)` | Appended when the last tool is confirmed done; `{tool}` / `{result}` placeholders |
|
|
196
207
|
| Grace period (ms) | `3000` | Wait after an interruption; cancelled if the host recovers on its own |
|
|
197
208
|
| Cooldown (ms) | `20000` | Min interval between auto-continues per session (failed attempts count too) |
|
|
198
209
|
| Max consecutive | `3` | Max consecutive auto-continues; stops until a user intervenes or a turn completes |
|
|
@@ -228,7 +239,7 @@ The plugin is browser-only and touches **no files, credentials, or network beyon
|
|
|
228
239
|
npm run typecheck # tsc --noEmit
|
|
229
240
|
npm run build # lib/client.js + lib/index.js + lib/types
|
|
230
241
|
npm run watch # rebuild on change; host HMR hot-reloads without a page refresh
|
|
231
|
-
npm run test # node tests/simulate.mjs —
|
|
242
|
+
npm run test # node tests/simulate.mjs — 34 behavioral scenarios
|
|
232
243
|
```
|
|
233
244
|
|
|
234
245
|
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
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
- **幂等护栏** — 续跑前检查上一步工具调用: 结果未确认(回合在工具执行中途夭折, 如 `git push` 可能已经推上去了)时, 续跑消息会提示模型先确认状态、不要重复执行; 工具已确认成功时说明已完成、请勿重复; 工具失败则不加护栏(重试本来就是目的)。两段护栏文本都可配置(支持 `{tool}` / `{result}` 占位符)
|
|
44
44
|
- **暂停** — 设置卡片里的全局 **暂停自动继续** 开关可立即停掉一切(实时 + 扫描); 会话级暂停(如通过通知按钮)只挂起单个会话, 到期自动恢复。唯一的显式例外是通知里的 **立即续跑** 按钮——按下它等于用户明确要求发送这一次, 不受暂停限制
|
|
45
45
|
- **通知按钮** — 通知带 **立即续跑**(无视冷却、连续上限与暂停, 马上发送)和 **暂停该会话 1 小时** 按钮
|
|
46
|
-
-
|
|
46
|
+
- **循环守卫** — 连**运行中的回合**也盯着: 连续多条短句且期间无工具调用(典型的「Let me read…」空转), 或同一工具被连续反复调用, 都会触发守卫: 取消当前回合并用可配置的循环提示文本重启(「停止重复, 换一种方式」)。取消带有内部来源标记, 绝不会与用户手动停止混淆——只有守卫发起的取消才会重启。阈值与提示文本都可配置
|
|
47
|
+
- **统计面板** — 设置卡片展示今日自动继续次数、恢复成功、继续后失败、永久性跳过、达上限停止、循环打断, 按错误码统计, 可一键清零
|
|
47
48
|
- **浏览器通知** — 可选: 自动继续成功 / 放弃 / 遇到永久性错误时弹出提醒; 首次使用时请求权限, 被拒绝后不再打扰
|
|
48
49
|
|
|
49
50
|
插件监听实时事件流, 对以下情况作出反应:
|
|
@@ -172,6 +173,11 @@ auto-continue:
|
|
|
172
173
|
backoffFactor: 2
|
|
173
174
|
backoffMaxMs: 300000
|
|
174
175
|
notify: false
|
|
176
|
+
loopGuard: true
|
|
177
|
+
loopShortChars: 40
|
|
178
|
+
loopShortCount: 8
|
|
179
|
+
loopToolRepeat: 4
|
|
180
|
+
loopText: '(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)'
|
|
175
181
|
```
|
|
176
182
|
|
|
177
183
|
**卡片操作说明:**
|
|
@@ -191,8 +197,13 @@ auto-continue:
|
|
|
191
197
|
| 继续文本 | `继续` | 中断后自动发送的消息内容 |
|
|
192
198
|
| 超限时的继续文本 | `继续` | 达到输出 token 上限时自动发送的文本(支持相同占位符) |
|
|
193
199
|
| 幂等护栏 | 开 | 续跑前检查上一步工具调用并给出指引(见「它做什么」) |
|
|
194
|
-
|
|
|
195
|
-
|
|
|
200
|
+
| 循环守卫 | 开 | 检测运行中的回合空转并重启(见「它做什么」) |
|
|
201
|
+
| 短句长度上限 (字符) | `40` | 模型消息文本短于该值计为一条短句(空转信号) |
|
|
202
|
+
| 连续短句阈值 | `8` | 连续多少条短句且期间无工具调用时判定空转循环 |
|
|
203
|
+
| 相同工具连续次数 | `4` | 连续调用同一工具多少次时判定死循环 |
|
|
204
|
+
| 循环提示文本 | `(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)` | 打断后重启回合时发送的文本; 支持 {tool} 占位符 |
|
|
205
|
+
| 结果未确认时的护栏文本 | `(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)` | 上一步工具可能已部分执行时附加; 支持 {tool} 占位符 |
|
|
206
|
+
| 工具已成功时的护栏文本 | `(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)` | 上一步工具已确认成功时附加; 支持 {tool} / {result} 占位符 |
|
|
196
207
|
| 宽限期 (ms) | `3000` | 中断后等待的时长; 期间宿主自行恢复则取消 |
|
|
197
208
|
| 冷却时间 (ms) | `20000` | 同一会话两次自动「继续」的最小间隔(失败尝试也计入) |
|
|
198
209
|
| 最大连续次数 | `3` | 同一会话连续自动「继续」上限; 超过后停止, 直到用户介入或成功回合 |
|
|
@@ -228,7 +239,7 @@ auto-continue:
|
|
|
228
239
|
npm run typecheck # tsc --noEmit
|
|
229
240
|
npm run build # lib/client.js + lib/index.js + lib/types
|
|
230
241
|
npm run watch # 监听变更自动重建; 宿主 HMR 免刷新热重载
|
|
231
|
-
npm run test # node tests/simulate.mjs —
|
|
242
|
+
npm run test # node tests/simulate.mjs — 34 个行为场景
|
|
232
243
|
```
|
|
233
244
|
|
|
234
245
|
`npm run watch` 运行时, profile 的 client-hmr 行每 500ms 轮询 `lib/client.js` 并在浏览器中热重载插件——改代码无需重启服务。
|
package/lib/client.js
CHANGED
|
@@ -58,7 +58,12 @@ var DEFAULT_CONFIG = {
|
|
|
58
58
|
backoffFactor: 2,
|
|
59
59
|
backoffMaxMs: 3e5,
|
|
60
60
|
notify: false,
|
|
61
|
-
paused: false
|
|
61
|
+
paused: false,
|
|
62
|
+
loopGuard: true,
|
|
63
|
+
loopShortChars: 40,
|
|
64
|
+
loopShortCount: 8,
|
|
65
|
+
loopToolRepeat: 4,
|
|
66
|
+
loopText: "(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)"
|
|
62
67
|
};
|
|
63
68
|
function numberOr(value, fallback) {
|
|
64
69
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
@@ -91,7 +96,12 @@ function resolveConfig(section) {
|
|
|
91
96
|
backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
|
|
92
97
|
backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
|
|
93
98
|
notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
|
|
94
|
-
paused: booleanOr(value.paused, DEFAULT_CONFIG.paused)
|
|
99
|
+
paused: booleanOr(value.paused, DEFAULT_CONFIG.paused),
|
|
100
|
+
loopGuard: booleanOr(value.loopGuard, DEFAULT_CONFIG.loopGuard),
|
|
101
|
+
loopShortChars: Math.max(1, numberOr(value.loopShortChars, DEFAULT_CONFIG.loopShortChars)),
|
|
102
|
+
loopShortCount: Math.max(2, numberOr(value.loopShortCount, DEFAULT_CONFIG.loopShortCount)),
|
|
103
|
+
loopToolRepeat: Math.max(2, numberOr(value.loopToolRepeat, DEFAULT_CONFIG.loopToolRepeat)),
|
|
104
|
+
loopText: typeof value.loopText === "string" && value.loopText.trim() !== "" ? value.loopText : DEFAULT_CONFIG.loopText
|
|
95
105
|
};
|
|
96
106
|
}
|
|
97
107
|
function isNonHumanReason(kind) {
|
|
@@ -285,7 +295,7 @@ function bumpStat(delta) {
|
|
|
285
295
|
const list = readStats();
|
|
286
296
|
let day = list.find((item) => item.date === todayKey());
|
|
287
297
|
if (day === void 0) {
|
|
288
|
-
day = { date: todayKey(), sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, byCode: {} };
|
|
298
|
+
day = { date: todayKey(), sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, looped: 0, byCode: {} };
|
|
289
299
|
list.unshift(day);
|
|
290
300
|
}
|
|
291
301
|
if (delta.sent !== void 0) day.sent += delta.sent;
|
|
@@ -293,13 +303,14 @@ function bumpStat(delta) {
|
|
|
293
303
|
if (delta.recovered !== void 0) day.recovered += delta.recovered;
|
|
294
304
|
if (delta.failed !== void 0) day.failed += delta.failed;
|
|
295
305
|
if (delta.gaveUp !== void 0) day.gaveUp += delta.gaveUp;
|
|
306
|
+
if (delta.looped !== void 0) day.looped += delta.looped;
|
|
296
307
|
if (delta.code !== void 0) day.byCode[delta.code] = (day.byCode[delta.code] ?? 0) + 1;
|
|
297
308
|
writeStats(list.slice(0, STATS_MAX_DAYS));
|
|
298
309
|
}
|
|
299
310
|
function readTodayStats() {
|
|
300
311
|
const today = todayKey();
|
|
301
312
|
const found = readStats().find((item) => item.date === today);
|
|
302
|
-
return found ?? { date: today, sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, byCode: {} };
|
|
313
|
+
return found ?? { date: today, sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, looped: 0, byCode: {} };
|
|
303
314
|
}
|
|
304
315
|
function resetTodayStats() {
|
|
305
316
|
writeStats(readStats().filter((item) => item.date !== todayKey()));
|
|
@@ -318,7 +329,11 @@ var freshState = () => ({
|
|
|
318
329
|
lastTool: void 0,
|
|
319
330
|
lastToolResult: void 0,
|
|
320
331
|
lastTurn: void 0,
|
|
321
|
-
pendingRecoveryAt: 0
|
|
332
|
+
pendingRecoveryAt: 0,
|
|
333
|
+
shortRun: 0,
|
|
334
|
+
toolRun: void 0,
|
|
335
|
+
loopFired: false,
|
|
336
|
+
loopCancelled: false
|
|
322
337
|
});
|
|
323
338
|
var RECOVERY_WINDOW_MS = 10 * 60 * 1e3;
|
|
324
339
|
function isOurEcho(state, event) {
|
|
@@ -429,12 +444,22 @@ var AutoContinueRunner = class {
|
|
|
429
444
|
const state = this.state(frame.sessionId);
|
|
430
445
|
state.lastTool = name;
|
|
431
446
|
state.lastToolResult = "pending";
|
|
447
|
+
state.shortRun = 0;
|
|
448
|
+
if (state.toolRun?.name === name) {
|
|
449
|
+
state.toolRun.count += 1;
|
|
450
|
+
} else {
|
|
451
|
+
state.toolRun = { name, count: 1 };
|
|
452
|
+
}
|
|
453
|
+
this.checkLoop(frame.sessionId, state);
|
|
432
454
|
}
|
|
433
455
|
} else if (frame.event.type === "tool/result") {
|
|
434
456
|
const state = this.state(frame.sessionId);
|
|
435
457
|
if (state.lastToolResult === "pending") {
|
|
436
458
|
state.lastToolResult = toolResultFacts(frame.event.data);
|
|
437
459
|
}
|
|
460
|
+
} else if (frame.event.type === "assistant/message") {
|
|
461
|
+
const state = this.state(frame.sessionId);
|
|
462
|
+
this.onAssistantMessage(frame.sessionId, state, frame.event);
|
|
438
463
|
}
|
|
439
464
|
this.onSessionEvent(frame.sessionId, frame.event);
|
|
440
465
|
break;
|
|
@@ -449,6 +474,65 @@ var AutoContinueRunner = class {
|
|
|
449
474
|
break;
|
|
450
475
|
}
|
|
451
476
|
}
|
|
477
|
+
/** 从 assistant/message 事件提取纯文本。 */
|
|
478
|
+
assistantText(event) {
|
|
479
|
+
const content = event.data.message.content;
|
|
480
|
+
if (!Array.isArray(content)) return "";
|
|
481
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* loop guard 信号 1(空转): 连续短句且期间无工具调用。
|
|
485
|
+
* 短句 = 模型消息文本短于 loopShortChars; 长句或工具调用都会重置计数。
|
|
486
|
+
*/
|
|
487
|
+
onAssistantMessage(sessionId, state, event) {
|
|
488
|
+
if (!this.getConfig().loopGuard) return;
|
|
489
|
+
const text = this.assistantText(event);
|
|
490
|
+
if (text.trim().length < this.getConfig().loopShortChars) {
|
|
491
|
+
state.shortRun += 1;
|
|
492
|
+
} else {
|
|
493
|
+
state.shortRun = 0;
|
|
494
|
+
}
|
|
495
|
+
this.checkLoop(sessionId, state);
|
|
496
|
+
}
|
|
497
|
+
/** 两个循环信号的公共检查; 命中且本回合未打断过则打断。 */
|
|
498
|
+
checkLoop(sessionId, state) {
|
|
499
|
+
if (!this.getConfig().loopGuard) return;
|
|
500
|
+
if (state.loopFired) return;
|
|
501
|
+
if (!state.running) return;
|
|
502
|
+
const config = this.getConfig();
|
|
503
|
+
if (state.shortRun >= config.loopShortCount) {
|
|
504
|
+
this.log(`检测到空转循环 ${sessionId}: 连续 ${state.shortRun} 条短句且无工具调用`);
|
|
505
|
+
void this.interruptLoop(sessionId, state);
|
|
506
|
+
} else if (state.toolRun !== void 0 && state.toolRun.count >= config.loopToolRepeat) {
|
|
507
|
+
this.log(`检测到工具死循环 ${sessionId}: 「${state.toolRun.name}」连续 ${state.toolRun.count} 次`);
|
|
508
|
+
void this.interruptLoop(sessionId, state);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* 打断运行中的回合: cancel(带来源标记)+ 进冷却。
|
|
513
|
+
* 随后的 turn/end aborted 会因 loopCancelled 走「可恢复中断」路径,
|
|
514
|
+
* 用 loopText 重启回合——不会与用户手动停止混淆。
|
|
515
|
+
*/
|
|
516
|
+
async interruptLoop(sessionId, state) {
|
|
517
|
+
if (state.loopFired) return;
|
|
518
|
+
if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) {
|
|
519
|
+
this.log(`跳过循环打断 ${sessionId}: 处于冷却期`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
state.loopFired = true;
|
|
523
|
+
state.loopCancelled = true;
|
|
524
|
+
state.lastAttemptAt = Date.now();
|
|
525
|
+
bumpStat({ looped: 1 });
|
|
526
|
+
try {
|
|
527
|
+
const response = await this.api.sessions.cancel({ sessionId });
|
|
528
|
+
this.log(
|
|
529
|
+
`已打断循环 ${sessionId}: ${response.result.ok ? "cancel 已受理" : "cancel 被拒绝"}`
|
|
530
|
+
);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.log(`打断循环失败 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
533
|
+
state.loopCancelled = false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
452
536
|
onSessionEvent(sessionId, event) {
|
|
453
537
|
const state = this.state(sessionId);
|
|
454
538
|
switch (event.type) {
|
|
@@ -456,6 +540,10 @@ var AutoContinueRunner = class {
|
|
|
456
540
|
state.running = true;
|
|
457
541
|
state.lastTool = void 0;
|
|
458
542
|
state.lastToolResult = void 0;
|
|
543
|
+
state.shortRun = 0;
|
|
544
|
+
state.toolRun = void 0;
|
|
545
|
+
state.loopFired = false;
|
|
546
|
+
state.loopCancelled = false;
|
|
459
547
|
this.cancelPending(sessionId, "宿主自行开启新回合");
|
|
460
548
|
break;
|
|
461
549
|
case "turn/end": {
|
|
@@ -467,8 +555,19 @@ var AutoContinueRunner = class {
|
|
|
467
555
|
state.lastFailure = void 0;
|
|
468
556
|
this.noteRecovery(sessionId, "completed");
|
|
469
557
|
} else if (reason.kind === "aborted") {
|
|
470
|
-
state.
|
|
471
|
-
|
|
558
|
+
if (state.loopCancelled) {
|
|
559
|
+
state.loopCancelled = false;
|
|
560
|
+
state.loopFired = false;
|
|
561
|
+
state.consecutive = 0;
|
|
562
|
+
state.pendingRecoveryAt = 0;
|
|
563
|
+
state.shortRun = 0;
|
|
564
|
+
state.toolRun = void 0;
|
|
565
|
+
state.lastAttemptAt = 0;
|
|
566
|
+
this.schedule(sessionId, "loop:aborted");
|
|
567
|
+
} else {
|
|
568
|
+
state.consecutive = 0;
|
|
569
|
+
state.pendingRecoveryAt = 0;
|
|
570
|
+
}
|
|
472
571
|
} else if (reason.kind === "blocked") {
|
|
473
572
|
} else if (reason.kind === "interrupted") {
|
|
474
573
|
state.consecutive = 0;
|
|
@@ -636,7 +735,7 @@ var AutoContinueRunner = class {
|
|
|
636
735
|
void this.fire(sessionId, reason);
|
|
637
736
|
}, config.graceMs);
|
|
638
737
|
state.pendingTimer = timer;
|
|
639
|
-
const template = reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
|
|
738
|
+
const template = reason.startsWith("loop:") ? config.loopText : reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
|
|
640
739
|
this.log(
|
|
641
740
|
`检测到非人为中断 ${sessionId}(${reason}), ${config.graceMs}ms 后自动发送「${template}」`
|
|
642
741
|
);
|
|
@@ -674,7 +773,7 @@ var AutoContinueRunner = class {
|
|
|
674
773
|
this.log(`跳过 ${sessionId}: 其他标签页正在发送`);
|
|
675
774
|
return;
|
|
676
775
|
}
|
|
677
|
-
const template = reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
|
|
776
|
+
const template = reason.startsWith("loop:") ? config.loopText : reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
|
|
678
777
|
let sessionTitle;
|
|
679
778
|
if (template.includes("{sessionTitle}")) {
|
|
680
779
|
sessionTitle = this.titles.get(sessionId);
|
|
@@ -946,6 +1045,17 @@ var zh = {
|
|
|
946
1045
|
"stats.recovered": "恢复成功",
|
|
947
1046
|
"stats.failed": "继续后仍失败",
|
|
948
1047
|
"stats.gaveUp": "停止(达上限)",
|
|
1048
|
+
"stats.looped": "循环打断",
|
|
1049
|
+
"field.loopGuard": "循环守卫",
|
|
1050
|
+
"field.loopGuardHint": "检测运行中的回合空转: 连续短句且无工具调用, 或连续调用相同工具时, 自动取消并用循环提示文本重启回合。",
|
|
1051
|
+
"field.loopShortChars": "短句长度上限 (字符)",
|
|
1052
|
+
"field.loopShortCharsHint": "模型消息文本短于该值计为一条短句(空转信号)。",
|
|
1053
|
+
"field.loopShortCount": "连续短句阈值",
|
|
1054
|
+
"field.loopShortCountHint": "连续多少条短句且期间无工具调用时判定空转循环。",
|
|
1055
|
+
"field.loopToolRepeat": "相同工具连续次数",
|
|
1056
|
+
"field.loopToolRepeatHint": "连续调用同一工具多少次时判定死循环。",
|
|
1057
|
+
"field.loopText": "循环提示文本",
|
|
1058
|
+
"field.loopTextHint": "打断后重启回合时发送的文本, 支持 {tool} 占位符。",
|
|
949
1059
|
"stats.byCode": "按错误码统计",
|
|
950
1060
|
"stats.empty": "今天还没有自动继续记录。",
|
|
951
1061
|
"stats.reset": "清零",
|
|
@@ -1016,6 +1126,17 @@ var en = {
|
|
|
1016
1126
|
"stats.recovered": "Recovered",
|
|
1017
1127
|
"stats.failed": "Failed after",
|
|
1018
1128
|
"stats.gaveUp": "Gave up (cap)",
|
|
1129
|
+
"stats.looped": "Loops broken",
|
|
1130
|
+
"field.loopGuard": "Loop guard",
|
|
1131
|
+
"field.loopGuardHint": "Detects a running turn spinning in place — many short sentences with no tool calls, or the same tool repeating — cancels it and restarts with the loop text.",
|
|
1132
|
+
"field.loopShortChars": "Short-sentence max (chars)",
|
|
1133
|
+
"field.loopShortCharsHint": "A model message shorter than this counts as a short sentence (spinning signal).",
|
|
1134
|
+
"field.loopShortCount": "Short-sentence threshold",
|
|
1135
|
+
"field.loopShortCountHint": "How many consecutive short sentences with no tool call trip the loop guard.",
|
|
1136
|
+
"field.loopToolRepeat": "Same-tool repeat count",
|
|
1137
|
+
"field.loopToolRepeatHint": "How many consecutive identical tool calls trip the loop guard.",
|
|
1138
|
+
"field.loopText": "Loop text",
|
|
1139
|
+
"field.loopTextHint": "Text sent after the loop guard restarts a turn; supports the {tool} placeholder.",
|
|
1019
1140
|
"stats.byCode": "By error code",
|
|
1020
1141
|
"stats.empty": "No auto-continue activity today.",
|
|
1021
1142
|
"stats.reset": "Reset",
|
|
@@ -1420,7 +1541,12 @@ var AutoContinueSettingsCardController = class {
|
|
|
1420
1541
|
booleanField("classify"),
|
|
1421
1542
|
numberField("backoffFactor", 1),
|
|
1422
1543
|
numberField("backoffMaxMs", 0),
|
|
1423
|
-
booleanField("notify")
|
|
1544
|
+
booleanField("notify"),
|
|
1545
|
+
booleanField("loopGuard"),
|
|
1546
|
+
numberField("loopShortChars", 1),
|
|
1547
|
+
numberField("loopShortCount", 2),
|
|
1548
|
+
numberField("loopToolRepeat", 2),
|
|
1549
|
+
textField("loopText")
|
|
1424
1550
|
]);
|
|
1425
1551
|
this.store = this.form.bind(() => this.projection(), import_client.createSnapshotStore);
|
|
1426
1552
|
}
|
|
@@ -1445,7 +1571,12 @@ var AutoContinueSettingsCardController = class {
|
|
|
1445
1571
|
classify: this.form.field("classify"),
|
|
1446
1572
|
backoffFactor: this.form.field("backoffFactor"),
|
|
1447
1573
|
backoffMaxMs: this.form.field("backoffMaxMs"),
|
|
1448
|
-
notify: this.form.field("notify")
|
|
1574
|
+
notify: this.form.field("notify"),
|
|
1575
|
+
loopGuard: this.form.field("loopGuard"),
|
|
1576
|
+
loopShortChars: this.form.field("loopShortChars"),
|
|
1577
|
+
loopShortCount: this.form.field("loopShortCount"),
|
|
1578
|
+
loopToolRepeat: this.form.field("loopToolRepeat"),
|
|
1579
|
+
loopText: this.form.field("loopText")
|
|
1449
1580
|
};
|
|
1450
1581
|
}
|
|
1451
1582
|
/**
|
|
@@ -1563,7 +1694,7 @@ function LivePanels(props) {
|
|
|
1563
1694
|
return () => clearInterval(timer);
|
|
1564
1695
|
}, []);
|
|
1565
1696
|
const stats = readTodayStats();
|
|
1566
|
-
const hasStats = stats.sent + stats.skipped + stats.recovered + stats.failed + stats.gaveUp > 0;
|
|
1697
|
+
const hasStats = stats.sent + stats.skipped + stats.recovered + stats.failed + stats.gaveUp + stats.looped > 0;
|
|
1567
1698
|
const codes = Object.entries(stats.byCode).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1568
1699
|
const paused = pausedSessions();
|
|
1569
1700
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
@@ -1604,6 +1735,10 @@ function LivePanels(props) {
|
|
|
1604
1735
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
1605
1736
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: t("stats.gaveUp") }),
|
|
1606
1737
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: stats.gaveUp })
|
|
1738
|
+
] }),
|
|
1739
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
1740
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", { children: t("stats.looped") }),
|
|
1741
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: stats.looped })
|
|
1607
1742
|
] })
|
|
1608
1743
|
] }),
|
|
1609
1744
|
codes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcCodes", children: [
|
|
@@ -1697,6 +1832,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1697
1832
|
...shared,
|
|
1698
1833
|
...state.continueText,
|
|
1699
1834
|
onEdit: (text) => props.edit("continueText", text),
|
|
1835
|
+
placeholder: DEFAULT_CONFIG.continueText,
|
|
1700
1836
|
onReset: () => props.resetField("continueText")
|
|
1701
1837
|
}
|
|
1702
1838
|
),
|
|
@@ -1709,6 +1845,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1709
1845
|
...shared,
|
|
1710
1846
|
...state.continueTextMaxTokens,
|
|
1711
1847
|
onEdit: (text) => props.edit("continueTextMaxTokens", text),
|
|
1848
|
+
placeholder: DEFAULT_CONFIG.continueTextMaxTokens,
|
|
1712
1849
|
onReset: () => props.resetField("continueTextMaxTokens")
|
|
1713
1850
|
}
|
|
1714
1851
|
),
|
|
@@ -1733,6 +1870,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1733
1870
|
...shared,
|
|
1734
1871
|
...state.guardPendingText,
|
|
1735
1872
|
onEdit: (text) => props.edit("guardPendingText", text),
|
|
1873
|
+
placeholder: DEFAULT_CONFIG.guardPendingText,
|
|
1736
1874
|
onReset: () => props.resetField("guardPendingText")
|
|
1737
1875
|
}
|
|
1738
1876
|
),
|
|
@@ -1745,6 +1883,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1745
1883
|
...shared,
|
|
1746
1884
|
...state.guardDoneText,
|
|
1747
1885
|
onEdit: (text) => props.edit("guardDoneText", text),
|
|
1886
|
+
placeholder: DEFAULT_CONFIG.guardDoneText,
|
|
1748
1887
|
onReset: () => props.resetField("guardDoneText")
|
|
1749
1888
|
}
|
|
1750
1889
|
),
|
|
@@ -1913,6 +2052,70 @@ function AutoContinueSettingsCard(props) {
|
|
|
1913
2052
|
onReset: () => props.resetField("notify")
|
|
1914
2053
|
}
|
|
1915
2054
|
),
|
|
2055
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2056
|
+
BooleanField,
|
|
2057
|
+
{
|
|
2058
|
+
id: "auto-continue-loop-guard",
|
|
2059
|
+
label: t("field.loopGuard"),
|
|
2060
|
+
hint: t("field.loopGuardHint"),
|
|
2061
|
+
...shared,
|
|
2062
|
+
...state.loopGuard,
|
|
2063
|
+
onEdit: (text) => props.edit("loopGuard", text),
|
|
2064
|
+
onReset: () => props.resetField("loopGuard")
|
|
2065
|
+
}
|
|
2066
|
+
),
|
|
2067
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2068
|
+
ValueField,
|
|
2069
|
+
{
|
|
2070
|
+
id: "auto-continue-loop-short-chars",
|
|
2071
|
+
label: t("field.loopShortChars"),
|
|
2072
|
+
hint: t("field.loopShortCharsHint"),
|
|
2073
|
+
numeric: true,
|
|
2074
|
+
...shared,
|
|
2075
|
+
...state.loopShortChars,
|
|
2076
|
+
onEdit: (text) => props.edit("loopShortChars", text),
|
|
2077
|
+
onReset: () => props.resetField("loopShortChars")
|
|
2078
|
+
}
|
|
2079
|
+
),
|
|
2080
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2081
|
+
ValueField,
|
|
2082
|
+
{
|
|
2083
|
+
id: "auto-continue-loop-short-count",
|
|
2084
|
+
label: t("field.loopShortCount"),
|
|
2085
|
+
hint: t("field.loopShortCountHint"),
|
|
2086
|
+
numeric: true,
|
|
2087
|
+
...shared,
|
|
2088
|
+
...state.loopShortCount,
|
|
2089
|
+
onEdit: (text) => props.edit("loopShortCount", text),
|
|
2090
|
+
onReset: () => props.resetField("loopShortCount")
|
|
2091
|
+
}
|
|
2092
|
+
),
|
|
2093
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2094
|
+
ValueField,
|
|
2095
|
+
{
|
|
2096
|
+
id: "auto-continue-loop-tool-repeat",
|
|
2097
|
+
label: t("field.loopToolRepeat"),
|
|
2098
|
+
hint: t("field.loopToolRepeatHint"),
|
|
2099
|
+
numeric: true,
|
|
2100
|
+
...shared,
|
|
2101
|
+
...state.loopToolRepeat,
|
|
2102
|
+
onEdit: (text) => props.edit("loopToolRepeat", text),
|
|
2103
|
+
onReset: () => props.resetField("loopToolRepeat")
|
|
2104
|
+
}
|
|
2105
|
+
),
|
|
2106
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2107
|
+
ValueField,
|
|
2108
|
+
{
|
|
2109
|
+
id: "auto-continue-loop-text",
|
|
2110
|
+
label: t("field.loopText"),
|
|
2111
|
+
hint: t("field.loopTextHint"),
|
|
2112
|
+
...shared,
|
|
2113
|
+
...state.loopText,
|
|
2114
|
+
onEdit: (text) => props.edit("loopText", text),
|
|
2115
|
+
placeholder: DEFAULT_CONFIG.loopText,
|
|
2116
|
+
onReset: () => props.resetField("loopText")
|
|
2117
|
+
}
|
|
2118
|
+
),
|
|
1916
2119
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LivePanels, { t })
|
|
1917
2120
|
]
|
|
1918
2121
|
}
|