dsh-client-auto-continue 0.5.7 → 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/lib/index.js CHANGED
@@ -7,6 +7,12 @@ var AutoContinueSchema = z.object({
7
7
  continueText: z.string().default("继续"),
8
8
  /** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
9
9
  continueTextMaxTokens: z.string().default("继续"),
10
+ /** Idempotency guard: inspect the last tool call before resuming and steer the model. */
11
+ guardTools: z.boolean().default(true),
12
+ /** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
13
+ guardPendingText: z.string().default("(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)"),
14
+ /** Guard text appended when the last tool call completed successfully (don't rerun it). */
15
+ guardDoneText: z.string().default("(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)"),
10
16
  /** Grace period after an interruption before auto-sending (ms). */
11
17
  graceMs: z.natural().default(3e3),
12
18
  /** Minimum interval between two auto-continues per session (ms). */
@@ -17,6 +17,12 @@ export interface AutoContinueSettings {
17
17
  continueText?: string;
18
18
  /** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
19
19
  continueTextMaxTokens?: string;
20
+ /** Idempotency guard: inspect the last tool call before resuming and steer the model. */
21
+ guardTools?: boolean;
22
+ /** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
23
+ guardPendingText?: string;
24
+ /** Guard text appended when the last tool call completed successfully (don't rerun it). */
25
+ guardDoneText?: string;
20
26
  /** Grace period after an interruption before auto-sending (ms). */
21
27
  graceMs?: number;
22
28
  /** Minimum interval between two auto-continues per session (ms). */
@@ -99,9 +105,18 @@ export interface TemplateContext {
99
105
  sessionTitle?: string;
100
106
  /** 自失败发生以来的毫秒数, 对应 {elapsed}。 */
101
107
  elapsedMs?: number;
108
+ /** 上一步工具结果摘要(截断), 对应 {result}(护栏模板用)。 */
109
+ result?: string;
102
110
  }
103
- /** 用失败事实与回合信息填充 continueText 模板占位符({code}/{message}/{status}/{tool}/{turn}/{errorCount}/{sessionTitle}/{elapsed})。 */
111
+ /** 用失败事实与回合信息填充 continueText 模板占位符({code}/{message}/{status}/{tool}/{turn}/{errorCount}/{sessionTitle}/{elapsed}/{result})。 */
104
112
  export declare function fillTemplate(template: string, ctx: TemplateContext): string;
113
+ /** 上一步工具调用的判定结果: 是否已确认完成, 以及文本摘要。 */
114
+ export interface ToolResultFacts {
115
+ /** 工具是否成功完成(内部失败或 isError 视为未成功)。 */
116
+ ok: boolean;
117
+ /** 工具输出的文本摘要(截断)。 */
118
+ excerpt: string;
119
+ }
105
120
  /** 自适应退避: 同一会话连续失败时的有效冷却间隔。 */
106
121
  export declare function effectiveCooldown(consecutive: number, base: number, factor: number, max: number): number;
107
122
  /** 暂停某会话: 到 `until` 之前, 引擎不会为该会话自动继续(通知按钮等调用)。 */
@@ -172,6 +187,16 @@ export declare class AutoContinueRunner {
172
187
  private schedule;
173
188
  private cancelPending;
174
189
  private fire;
190
+ /**
191
+ * 组装本次续跑消息: 模板填充 + 幂等护栏。
192
+ * 护栏依据上一步工具调用的执行状态附加指引, 防止重跑副作用操作:
193
+ * - 结果未确认(可能已部分执行)→ 提示先确认状态、不要重复执行
194
+ * - 已确认成功 → 提示已完成、不要重复执行
195
+ * - 已失败 → 不加护栏(重试工具本来就是目的)
196
+ */
197
+ private buildContinueText;
198
+ /** 上一步工具调用的护栏状态(实时路径, 由 mux 帧维护)。 */
199
+ private currentGuard;
175
200
  /** 会话标题缓存(来自 session.list 投影, {sessionTitle} 占位符用)。 */
176
201
  private readonly titles;
177
202
  /** 查一次 session.list, 顺带缓存该会话的标题。 */
@@ -186,4 +211,6 @@ export declare class AutoContinueRunner {
186
211
  * @returns 是否成功完成一次扫描(宿主就绪)。
187
212
  */
188
213
  private scanInterrupted;
214
+ /** 从历史事件恢复上一步工具调用状态(扫描路径的幂等护栏)。 */
215
+ private applyGuardFromEvents;
189
216
  }
@@ -13,6 +13,12 @@ export declare const zh: {
13
13
  'field.continueTextHint': string;
14
14
  'field.continueTextMaxTokens': string;
15
15
  'field.continueTextMaxTokensHint': string;
16
+ 'field.guardTools': string;
17
+ 'field.guardToolsHint': string;
18
+ 'field.guardPendingText': string;
19
+ 'field.guardPendingTextHint': string;
20
+ 'field.guardDoneText': string;
21
+ 'field.guardDoneTextHint': string;
16
22
  'field.graceMs': string;
17
23
  'field.graceMsHint': string;
18
24
  'field.cooldownMs': string;
@@ -7,6 +7,9 @@ export interface AutoContinueSettingsCardState extends CardShell {
7
7
  paused: CardFieldState;
8
8
  continueText: CardFieldState;
9
9
  continueTextMaxTokens: CardFieldState;
10
+ guardTools: CardFieldState;
11
+ guardPendingText: CardFieldState;
12
+ guardDoneText: CardFieldState;
10
13
  graceMs: CardFieldState;
11
14
  cooldownMs: CardFieldState;
12
15
  maxConsecutive: CardFieldState;
@@ -13,6 +13,12 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
13
13
  continueText: z<string, string>;
14
14
  /** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
15
15
  continueTextMaxTokens: z<string, string>;
16
+ /** Idempotency guard: inspect the last tool call before resuming and steer the model. */
17
+ guardTools: z<boolean, boolean>;
18
+ /** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
19
+ guardPendingText: z<string, string>;
20
+ /** Guard text appended when the last tool call completed successfully (don't rerun it). */
21
+ guardDoneText: z<string, string>;
16
22
  /** Grace period after an interruption before auto-sending (ms). */
17
23
  graceMs: z<number, number>;
18
24
  /** Minimum interval between two auto-continues per session (ms). */
@@ -46,6 +52,12 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
46
52
  continueText: z<string, string>;
47
53
  /** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
48
54
  continueTextMaxTokens: z<string, string>;
55
+ /** Idempotency guard: inspect the last tool call before resuming and steer the model. */
56
+ guardTools: z<boolean, boolean>;
57
+ /** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
58
+ guardPendingText: z<string, string>;
59
+ /** Guard text appended when the last tool call completed successfully (don't rerun it). */
60
+ guardDoneText: z<string, string>;
49
61
  /** Grace period after an interruption before auto-sending (ms). */
50
62
  graceMs: z<number, number>;
51
63
  /** Minimum interval between two auto-continues per session (ms). */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-client-auto-continue",
3
3
  "description": "DSH Web UI plugin: automatically sends \"继续\" (continue) when a request is interrupted by network errors or other non-human causes",
4
- "version": "0.5.7",
4
+ "version": "0.6.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -26,6 +26,12 @@ export interface AutoContinueSettings {
26
26
  continueText?: string;
27
27
  /** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
28
28
  continueTextMaxTokens?: string;
29
+ /** Idempotency guard: inspect the last tool call before resuming and steer the model. */
30
+ guardTools?: boolean;
31
+ /** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
32
+ guardPendingText?: string;
33
+ /** Guard text appended when the last tool call completed successfully (don't rerun it). */
34
+ guardDoneText?: string;
29
35
  /** Grace period after an interruption before auto-sending (ms). */
30
36
  graceMs?: number;
31
37
  /** Minimum interval between two auto-continues per session (ms). */
@@ -63,6 +69,9 @@ export type AutoContinueConfig = Required<AutoContinueSettings>;
63
69
  export const DEFAULT_CONFIG: AutoContinueConfig = {
64
70
  continueText: '继续',
65
71
  continueTextMaxTokens: '继续',
72
+ guardTools: true,
73
+ guardPendingText: '(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)',
74
+ guardDoneText: '(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)',
66
75
  graceMs: 3000,
67
76
  cooldownMs: 20000,
68
77
  maxConsecutive: 3,
@@ -98,9 +107,20 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
98
107
  typeof value.continueTextMaxTokens === 'string' && value.continueTextMaxTokens.trim() !== ''
99
108
  ? value.continueTextMaxTokens
100
109
  : DEFAULT_CONFIG.continueTextMaxTokens;
110
+ const guardPendingText =
111
+ typeof value.guardPendingText === 'string' && value.guardPendingText.trim() !== ''
112
+ ? value.guardPendingText
113
+ : DEFAULT_CONFIG.guardPendingText;
114
+ const guardDoneText =
115
+ typeof value.guardDoneText === 'string' && value.guardDoneText.trim() !== ''
116
+ ? value.guardDoneText
117
+ : DEFAULT_CONFIG.guardDoneText;
101
118
  return {
102
119
  continueText: text,
103
120
  continueTextMaxTokens: maxTokensText,
121
+ guardTools: booleanOr(value.guardTools, DEFAULT_CONFIG.guardTools),
122
+ guardPendingText,
123
+ guardDoneText,
104
124
  graceMs: numberOr(value.graceMs, DEFAULT_CONFIG.graceMs),
105
125
  cooldownMs: numberOr(value.cooldownMs, DEFAULT_CONFIG.cooldownMs),
106
126
  maxConsecutive: Math.max(1, numberOr(value.maxConsecutive, DEFAULT_CONFIG.maxConsecutive)),
@@ -249,9 +269,11 @@ export interface TemplateContext {
249
269
  sessionTitle?: string;
250
270
  /** 自失败发生以来的毫秒数, 对应 {elapsed}。 */
251
271
  elapsedMs?: number;
272
+ /** 上一步工具结果摘要(截断), 对应 {result}(护栏模板用)。 */
273
+ result?: string;
252
274
  }
253
275
 
254
- /** 用失败事实与回合信息填充 continueText 模板占位符({code}/{message}/{status}/{tool}/{turn}/{errorCount}/{sessionTitle}/{elapsed})。 */
276
+ /** 用失败事实与回合信息填充 continueText 模板占位符({code}/{message}/{status}/{tool}/{turn}/{errorCount}/{sessionTitle}/{elapsed}/{result})。 */
255
277
  export function fillTemplate(template: string, ctx: TemplateContext): string {
256
278
  return template
257
279
  .replace(/\{code\}/g, ctx.facts?.code ?? '')
@@ -261,7 +283,51 @@ export function fillTemplate(template: string, ctx: TemplateContext): string {
261
283
  .replace(/\{turn\}/g, ctx.turn !== undefined ? String(ctx.turn) : '')
262
284
  .replace(/\{errorCount\}/g, ctx.errorCount !== undefined ? String(ctx.errorCount) : '')
263
285
  .replace(/\{sessionTitle\}/g, ctx.sessionTitle ?? '')
264
- .replace(/\{elapsed\}/g, formatElapsed(ctx.elapsedMs));
286
+ .replace(/\{elapsed\}/g, formatElapsed(ctx.elapsedMs))
287
+ .replace(/\{result\}/g, ctx.result ?? '');
288
+ }
289
+
290
+ // ---------- 幂等护栏: 上一步工具调用的执行状态 ----------
291
+
292
+ /** 工具结果摘要的最大长度(护栏模板 {result} 用)。 */
293
+ const TOOL_RESULT_CAP = 160;
294
+
295
+ /** 从任意内容块里递归收集文本(结果为模型可见的工具输出)。 */
296
+ function extractText(blocks: unknown, cap: number): string {
297
+ let out = '';
298
+ const walk = (value: unknown): void => {
299
+ if (out.length >= cap) return;
300
+ if (Array.isArray(value)) {
301
+ for (const item of value) walk(item);
302
+ return;
303
+ }
304
+ if (typeof value !== 'object' || value === null) return;
305
+ const record = value as Record<string, unknown>;
306
+ if (record['type'] === 'text' && typeof record['text'] === 'string') {
307
+ out += record['text'];
308
+ return;
309
+ }
310
+ for (const child of Object.values(record)) walk(child);
311
+ };
312
+ walk(blocks);
313
+ return out.slice(0, cap);
314
+ }
315
+
316
+ /** 上一步工具调用的判定结果: 是否已确认完成, 以及文本摘要。 */
317
+ export interface ToolResultFacts {
318
+ /** 工具是否成功完成(内部失败或 isError 视为未成功)。 */
319
+ ok: boolean;
320
+ /** 工具输出的文本摘要(截断)。 */
321
+ excerpt: string;
322
+ }
323
+
324
+ /** 从 tool/result 事件载荷提取成功与否与文本摘要。 */
325
+ function toolResultFacts(data: {
326
+ error?: { name?: string; code?: string };
327
+ message?: { content?: Array<{ type?: string; content?: unknown; isError?: boolean }> };
328
+ }): ToolResultFacts {
329
+ const failed = data.error !== undefined || data.message?.content?.[0]?.isError === true;
330
+ return { ok: !failed, excerpt: extractText(data.message?.content?.[0]?.content, TOOL_RESULT_CAP) };
265
331
  }
266
332
 
267
333
  /** 自适应退避: 同一会话连续失败时的有效冷却间隔。 */
@@ -493,8 +559,10 @@ interface SessionState {
493
559
  lastFailure: FailureFacts | undefined;
494
560
  /** 最近一次失败的发生时间(模板 {elapsed} 与恢复统计用)。 */
495
561
  lastFailureAt: number;
496
- /** 失败前最后一次工具调用的名称(模板 {tool})。 */
562
+ /** 失败前最后一次工具调用的名称(模板 {tool} 与幂等护栏用)。 */
497
563
  lastTool: string | undefined;
564
+ /** 上一步工具调用的结果状态: 'pending' = 已发起未见结果(可能已部分执行)。 */
565
+ lastToolResult: 'pending' | ToolResultFacts | undefined;
498
566
  /** 失败回合的编号(模板 {turn})。 */
499
567
  lastTurn: number | undefined;
500
568
  /** 我们最近一次自动发送的时间戳; 0 = 没有待确认的恢复。 */
@@ -513,6 +581,7 @@ const freshState = (): SessionState => ({
513
581
  lastFailure: undefined,
514
582
  lastFailureAt: 0,
515
583
  lastTool: undefined,
584
+ lastToolResult: undefined,
516
585
  lastTurn: undefined,
517
586
  pendingRecoveryAt: 0,
518
587
  });
@@ -657,7 +726,16 @@ export class AutoContinueRunner {
657
726
  case 'session/event':
658
727
  if (frame.event.type === 'tool/call') {
659
728
  const name = frame.event.data.name;
660
- if (typeof name === 'string') this.state(frame.sessionId).lastTool = name;
729
+ if (typeof name === 'string') {
730
+ const state = this.state(frame.sessionId);
731
+ state.lastTool = name;
732
+ state.lastToolResult = 'pending'; // 已发起, 尚未见结果
733
+ }
734
+ } else if (frame.event.type === 'tool/result') {
735
+ const state = this.state(frame.sessionId);
736
+ if (state.lastToolResult === 'pending') {
737
+ state.lastToolResult = toolResultFacts(frame.event.data);
738
+ }
661
739
  }
662
740
  this.onSessionEvent(frame.sessionId, frame.event);
663
741
  break;
@@ -678,6 +756,9 @@ export class AutoContinueRunner {
678
756
  switch (event.type) {
679
757
  case 'turn/start':
680
758
  state.running = true;
759
+ // 新回合开始: 清空上一步工具调用状态, 避免跨回合误用护栏
760
+ state.lastTool = undefined;
761
+ state.lastToolResult = undefined;
681
762
  this.cancelPending(sessionId, '宿主自行开启新回合');
682
763
  break;
683
764
  case 'turn/end': {
@@ -930,14 +1011,7 @@ export class AutoContinueRunner {
930
1011
  sessionTitle = info?.title;
931
1012
  }
932
1013
  }
933
- const text = fillTemplate(template, {
934
- facts: state.lastFailure,
935
- tool: state.lastTool,
936
- turn: state.lastTurn,
937
- errorCount: state.consecutive + 1,
938
- sessionTitle,
939
- elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : undefined,
940
- });
1014
+ const text = this.buildContinueText(config, state, template, sessionTitle);
941
1015
  const zone = clientTimeZone();
942
1016
  state.lastAttemptAt = Date.now(); // 先记账: 无论成败, 本次尝试都进入冷却
943
1017
  try {
@@ -986,6 +1060,51 @@ export class AutoContinueRunner {
986
1060
  }
987
1061
  }
988
1062
 
1063
+ /**
1064
+ * 组装本次续跑消息: 模板填充 + 幂等护栏。
1065
+ * 护栏依据上一步工具调用的执行状态附加指引, 防止重跑副作用操作:
1066
+ * - 结果未确认(可能已部分执行)→ 提示先确认状态、不要重复执行
1067
+ * - 已确认成功 → 提示已完成、不要重复执行
1068
+ * - 已失败 → 不加护栏(重试工具本来就是目的)
1069
+ */
1070
+ private buildContinueText(
1071
+ config: AutoContinueConfig,
1072
+ state: SessionState,
1073
+ template: string,
1074
+ sessionTitle: string | undefined,
1075
+ ): string {
1076
+ let text = fillTemplate(template, {
1077
+ facts: state.lastFailure,
1078
+ tool: state.lastTool,
1079
+ turn: state.lastTurn,
1080
+ errorCount: state.consecutive + 1,
1081
+ sessionTitle,
1082
+ elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : undefined,
1083
+ });
1084
+ if (!config.guardTools) return text;
1085
+ const guard = this.currentGuard(state);
1086
+ if (guard.kind === 'pending') {
1087
+ text += ` ${fillTemplate(config.guardPendingText, { tool: guard.tool, result: guard.result })}`;
1088
+ } else if (guard.kind === 'done') {
1089
+ text += ` ${fillTemplate(config.guardDoneText, { tool: guard.tool, result: guard.result })}`;
1090
+ }
1091
+ return text;
1092
+ }
1093
+
1094
+ /** 上一步工具调用的护栏状态(实时路径, 由 mux 帧维护)。 */
1095
+ private currentGuard(state: SessionState): {
1096
+ kind: 'none' | 'pending' | 'done' | 'failed';
1097
+ tool?: string;
1098
+ result?: string;
1099
+ } {
1100
+ if (state.lastTool === undefined || state.lastToolResult === undefined) return { kind: 'none' };
1101
+ if (state.lastToolResult === 'pending') return { kind: 'pending', tool: state.lastTool };
1102
+ if (state.lastToolResult.ok) {
1103
+ return { kind: 'done', tool: state.lastTool, result: state.lastToolResult.excerpt };
1104
+ }
1105
+ return { kind: 'failed', tool: state.lastTool };
1106
+ }
1107
+
989
1108
  /** 会话标题缓存(来自 session.list 投影, {sessionTitle} 占位符用)。 */
990
1109
  private readonly titles = new Map<SessionId, string>();
991
1110
 
@@ -1108,9 +1227,38 @@ export class AutoContinueRunner {
1108
1227
  if (superseded) break;
1109
1228
  }
1110
1229
  if (superseded) continue;
1230
+ // 幂等护栏: 从历史事件里重建上一步工具调用的执行状态
1231
+ this.applyGuardFromEvents(state, events, lastEnd.seq);
1111
1232
  this.log(`扫描发现中断 ${summary.sessionId}(turn/end:${reason.kind}), 安排自动继续`);
1112
1233
  this.schedule(summary.sessionId, `scan:turn/end:${reason.kind}`);
1113
1234
  }
1114
1235
  return true;
1115
1236
  }
1237
+
1238
+ /** 从历史事件恢复上一步工具调用状态(扫描路径的幂等护栏)。 */
1239
+ private applyGuardFromEvents(
1240
+ state: SessionState,
1241
+ events: { event: SessionEvent }[],
1242
+ untilSeq: number,
1243
+ ): void {
1244
+ state.lastTool = undefined;
1245
+ state.lastToolResult = undefined;
1246
+ let call: SessionEvent<'tool/call'> | undefined;
1247
+ for (const entry of events) {
1248
+ const event = entry.event;
1249
+ if (event.seq >= untilSeq) continue;
1250
+ if (event.type === 'tool/call') call = event;
1251
+ }
1252
+ if (call === undefined) return;
1253
+ state.lastTool = call.data.name;
1254
+ state.lastToolResult = 'pending';
1255
+ for (const entry of events) {
1256
+ const event = entry.event;
1257
+ if (event.seq <= call.seq || event.seq >= untilSeq) continue;
1258
+ if (event.type === 'tool/result') {
1259
+ state.lastToolResult = toolResultFacts(event.data);
1260
+ break;
1261
+ }
1262
+ }
1263
+ }
1116
1264
  }
@@ -14,6 +14,12 @@ export const zh = {
14
14
  'field.continueTextHint': '中断后自动发送的消息内容。',
15
15
  'field.continueTextMaxTokens': '超限时的继续文本',
16
16
  'field.continueTextMaxTokensHint': '达到输出 token 上限时自动发送的文本, 支持与继续文本相同的占位符。',
17
+ 'field.guardTools': '幂等护栏',
18
+ 'field.guardToolsHint': '续跑前检查上一步工具调用: 结果未确认时提示先确认状态, 已成功时提示不要重复执行, 避免重复 commit/调 API。',
19
+ 'field.guardPendingText': '结果未确认时的护栏文本',
20
+ 'field.guardPendingTextHint': '上一步工具可能已部分执行时附加到继续文本之后, 支持 {tool} 占位符。',
21
+ 'field.guardDoneText': '工具已成功时的护栏文本',
22
+ 'field.guardDoneTextHint': '上一步工具已确认成功时附加到继续文本之后, 支持 {tool} 与 {result}(结果摘要)占位符。',
17
23
  'field.graceMs': '宽限期 (ms)',
18
24
  'field.graceMsHint': '检测到中断后等待的时长; 期间宿主自行恢复则取消。',
19
25
  'field.cooldownMs': '冷却时间 (ms)',
@@ -83,6 +89,12 @@ export const en: Record<SettingsCardKey, string> = {
83
89
  'field.continueTextHint': 'Message automatically sent after an interruption.',
84
90
  'field.continueTextMaxTokens': 'Continue text (max tokens)',
85
91
  'field.continueTextMaxTokensHint': 'Text sent when the output token ceiling is reached; same placeholders as the continue text.',
92
+ 'field.guardTools': 'Idempotency guard',
93
+ '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.',
94
+ 'field.guardPendingText': 'Guard text (unconfirmed result)',
95
+ 'field.guardPendingTextHint': 'Appended when the last tool may have partially executed; supports the {tool} placeholder.',
96
+ 'field.guardDoneText': 'Guard text (tool succeeded)',
97
+ 'field.guardDoneTextHint': 'Appended when the last tool is confirmed done; supports {tool} and {result} (result excerpt).',
86
98
  'field.graceMs': 'Grace period (ms)',
87
99
  'field.graceMsHint': 'Wait after an interruption; cancelled if the host recovers on its own.',
88
100
  'field.cooldownMs': 'Cooldown (ms)',
@@ -38,6 +38,9 @@ export interface AutoContinueSettingsCardState extends CardShell {
38
38
  paused: CardFieldState;
39
39
  continueText: CardFieldState;
40
40
  continueTextMaxTokens: CardFieldState;
41
+ guardTools: CardFieldState;
42
+ guardPendingText: CardFieldState;
43
+ guardDoneText: CardFieldState;
41
44
  graceMs: CardFieldState;
42
45
  cooldownMs: CardFieldState;
43
46
  maxConsecutive: CardFieldState;
@@ -74,6 +77,9 @@ export class AutoContinueSettingsCardController {
74
77
  booleanField('paused'),
75
78
  textField('continueText'),
76
79
  textField('continueTextMaxTokens'),
80
+ booleanField('guardTools'),
81
+ textField('guardPendingText'),
82
+ textField('guardDoneText'),
77
83
  numberField('graceMs', 0),
78
84
  numberField('cooldownMs', 0),
79
85
  numberField('maxConsecutive', 1),
@@ -97,6 +103,9 @@ export class AutoContinueSettingsCardController {
97
103
  paused: this.form.field('paused'),
98
104
  continueText: this.form.field('continueText'),
99
105
  continueTextMaxTokens: this.form.field('continueTextMaxTokens'),
106
+ guardTools: this.form.field('guardTools'),
107
+ guardPendingText: this.form.field('guardPendingText'),
108
+ guardDoneText: this.form.field('guardDoneText'),
100
109
  graceMs: this.form.field('graceMs'),
101
110
  cooldownMs: this.form.field('cooldownMs'),
102
111
  maxConsecutive: this.form.field('maxConsecutive'),
@@ -415,6 +424,33 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
415
424
  onEdit={(text) => props.edit('continueTextMaxTokens', text)}
416
425
  onReset={() => props.resetField('continueTextMaxTokens')}
417
426
  />
427
+ <BooleanField
428
+ id="auto-continue-guard-tools"
429
+ label={t('field.guardTools')}
430
+ hint={t('field.guardToolsHint')}
431
+ {...shared}
432
+ {...state.guardTools}
433
+ onEdit={(text) => props.edit('guardTools', text)}
434
+ onReset={() => props.resetField('guardTools')}
435
+ />
436
+ <ValueField
437
+ id="auto-continue-guard-pending-text"
438
+ label={t('field.guardPendingText')}
439
+ hint={t('field.guardPendingTextHint')}
440
+ {...shared}
441
+ {...state.guardPendingText}
442
+ onEdit={(text) => props.edit('guardPendingText', text)}
443
+ onReset={() => props.resetField('guardPendingText')}
444
+ />
445
+ <ValueField
446
+ id="auto-continue-guard-done-text"
447
+ label={t('field.guardDoneText')}
448
+ hint={t('field.guardDoneTextHint')}
449
+ {...shared}
450
+ {...state.guardDoneText}
451
+ onEdit={(text) => props.edit('guardDoneText', text)}
452
+ onReset={() => props.resetField('guardDoneText')}
453
+ />
418
454
  <ValueField
419
455
  id="auto-continue-grace-ms"
420
456
  label={t('field.graceMs')}
package/src/index.ts CHANGED
@@ -18,6 +18,16 @@ export const AutoContinueSchema = z.object({
18
18
  continueText: z.string().default('继续'),
19
19
  /** Text sent when the output token ceiling is reached (same placeholders as `continueText`). */
20
20
  continueTextMaxTokens: z.string().default('继续'),
21
+ /** Idempotency guard: inspect the last tool call before resuming and steer the model. */
22
+ guardTools: z.boolean().default(true),
23
+ /** Guard text appended when the last tool call has no confirmed result (it may have partially executed). */
24
+ guardPendingText: z
25
+ .string()
26
+ .default('(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)'),
27
+ /** Guard text appended when the last tool call completed successfully (don't rerun it). */
28
+ guardDoneText: z
29
+ .string()
30
+ .default('(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)'),
21
31
  /** Grace period after an interruption before auto-sending (ms). */
22
32
  graceMs: z.natural().default(3000),
23
33
  /** Minimum interval between two auto-continues per session (ms). */