dsh-client-auto-continue 0.7.1 → 0.7.2

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
@@ -45,10 +45,12 @@ var AutoContinueSchema = z.object({
45
45
  loopGuard: z.boolean().default(true),
46
46
  /** A model message shorter than this many chars counts as a short sentence (loop signal). */
47
47
  loopShortChars: z.natural().min(1).default(40),
48
- /** Consecutive short sentences with no tool call in between trip the loop guard. */
49
- loopShortCount: z.natural().min(2).default(8),
50
- /** Consecutive identical tool calls trip the loop guard. */
51
- loopToolRepeat: z.natural().min(2).default(4),
48
+ /** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
49
+ loopWindowMs: z.natural().min(1e3).default(3e4),
50
+ /** Consecutive short sentences trip the loop guard. */
51
+ loopShortCount: z.natural().min(2).default(12),
52
+ /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
53
+ loopToolRepeat: z.natural().min(2).default(5),
52
54
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
53
55
  loopText: z.string().default("(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)")
54
56
  });
@@ -55,9 +55,11 @@ export interface AutoContinueSettings {
55
55
  loopGuard?: boolean;
56
56
  /** A model message shorter than this many chars counts as a "short sentence" (loop signal). */
57
57
  loopShortChars?: number;
58
- /** Consecutive short sentences with no tool call in between trip the loop guard. */
58
+ /** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
59
+ loopWindowMs?: number;
60
+ /** Consecutive short sentences trip the loop guard. */
59
61
  loopShortCount?: number;
60
- /** Consecutive identical tool calls trip the loop guard. */
62
+ /** Consecutive identical tool calls with identical arguments AND identical results trip the loop guard. */
61
63
  loopToolRepeat?: number;
62
64
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
63
65
  loopText?: string;
@@ -186,8 +188,9 @@ export declare class AutoContinueRunner {
186
188
  /** 从 assistant/message 事件提取纯文本。 */
187
189
  private assistantText;
188
190
  /**
189
- * loop guard 信号 1(空转): 连续短句且期间无工具调用。
190
- * 短句 = 模型消息文本短于 loopShortChars; 长句或工具调用都会重置计数。
191
+ * loop guard 信号 1(空转): 时间窗内连续短句且期间无工具调用。
192
+ * 短句 = 模型消息文本短于 loopShortChars; 长句、工具调用、或短句间隔超过
193
+ * loopWindowMs(正常思考的短文本散布在长时间里)都会重置计数。
191
194
  */
192
195
  private onAssistantMessage;
193
196
  /** 两个循环信号的公共检查; 命中且本回合未打断过则打断。 */
@@ -56,6 +56,8 @@ export declare const zh: {
56
56
  'field.loopGuardHint': string;
57
57
  'field.loopShortChars': string;
58
58
  'field.loopShortCharsHint': string;
59
+ 'field.loopWindowMs': string;
60
+ 'field.loopWindowMsHint': string;
59
61
  'field.loopShortCount': string;
60
62
  'field.loopShortCountHint': string;
61
63
  'field.loopToolRepeat': string;
@@ -25,6 +25,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
25
25
  notify: CardFieldState;
26
26
  loopGuard: CardFieldState;
27
27
  loopShortChars: CardFieldState;
28
+ loopWindowMs: CardFieldState;
28
29
  loopShortCount: CardFieldState;
29
30
  loopToolRepeat: CardFieldState;
30
31
  loopText: CardFieldState;
@@ -51,9 +51,11 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
51
51
  loopGuard: z<boolean, boolean>;
52
52
  /** A model message shorter than this many chars counts as a short sentence (loop signal). */
53
53
  loopShortChars: z<number, number>;
54
- /** Consecutive short sentences with no tool call in between trip the loop guard. */
54
+ /** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
55
+ loopWindowMs: z<number, number>;
56
+ /** Consecutive short sentences trip the loop guard. */
55
57
  loopShortCount: z<number, number>;
56
- /** Consecutive identical tool calls trip the loop guard. */
58
+ /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
57
59
  loopToolRepeat: z<number, number>;
58
60
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
59
61
  loopText: z<string, string>;
@@ -100,9 +102,11 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
100
102
  loopGuard: z<boolean, boolean>;
101
103
  /** A model message shorter than this many chars counts as a short sentence (loop signal). */
102
104
  loopShortChars: z<number, number>;
103
- /** Consecutive short sentences with no tool call in between trip the loop guard. */
105
+ /** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
106
+ loopWindowMs: z<number, number>;
107
+ /** Consecutive short sentences trip the loop guard. */
104
108
  loopShortCount: z<number, number>;
105
- /** Consecutive identical tool calls trip the loop guard. */
109
+ /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
106
110
  loopToolRepeat: z<number, number>;
107
111
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
108
112
  loopText: z<string, string>;
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.7.1",
4
+ "version": "0.7.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -64,9 +64,11 @@ export interface AutoContinueSettings {
64
64
  loopGuard?: boolean;
65
65
  /** A model message shorter than this many chars counts as a "short sentence" (loop signal). */
66
66
  loopShortChars?: number;
67
- /** Consecutive short sentences with no tool call in between trip the loop guard. */
67
+ /** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
68
+ loopWindowMs?: number;
69
+ /** Consecutive short sentences trip the loop guard. */
68
70
  loopShortCount?: number;
69
- /** Consecutive identical tool calls trip the loop guard. */
71
+ /** Consecutive identical tool calls with identical arguments AND identical results trip the loop guard. */
70
72
  loopToolRepeat?: number;
71
73
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
72
74
  loopText?: string;
@@ -98,8 +100,9 @@ export const DEFAULT_CONFIG: AutoContinueConfig = {
98
100
  paused: false,
99
101
  loopGuard: true,
100
102
  loopShortChars: 40,
101
- loopShortCount: 8,
102
- loopToolRepeat: 4,
103
+ loopWindowMs: 30000,
104
+ loopShortCount: 12,
105
+ loopToolRepeat: 5,
103
106
  loopText: '(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)',
104
107
  };
105
108
 
@@ -152,6 +155,7 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
152
155
  paused: booleanOr(value.paused, DEFAULT_CONFIG.paused),
153
156
  loopGuard: booleanOr(value.loopGuard, DEFAULT_CONFIG.loopGuard),
154
157
  loopShortChars: Math.max(1, numberOr(value.loopShortChars, DEFAULT_CONFIG.loopShortChars)),
158
+ loopWindowMs: Math.max(1000, numberOr(value.loopWindowMs, DEFAULT_CONFIG.loopWindowMs)),
155
159
  loopShortCount: Math.max(2, numberOr(value.loopShortCount, DEFAULT_CONFIG.loopShortCount)),
156
160
  loopToolRepeat: Math.max(2, numberOr(value.loopToolRepeat, DEFAULT_CONFIG.loopToolRepeat)),
157
161
  loopText:
@@ -596,8 +600,24 @@ interface SessionState {
596
600
  pendingRecoveryAt: number;
597
601
  /** 当前连续短句数(loop guard 信号 1: 空转)。 */
598
602
  shortRun: number;
599
- /** 当前连续相同工具调用(loop guard 信号 2: 死循环)。 */
600
- toolRun: { name: string; count: number } | undefined;
603
+ /** 最后一条短句的时间(时间窗判定用)。 */
604
+ lastShortAt: number;
605
+ /**
606
+ * 工具重复信号(loop guard 信号 2: 死循环)。
607
+ * 只有「同工具 + 同参数 + 同结果」的连续调用才累计; 参数或结果有变化视为有进展, 计数重置。
608
+ */
609
+ toolRun:
610
+ | {
611
+ /** 工具名 + 参数(用于判定是否同一调用)。 */
612
+ key: string;
613
+ /** 连续相同调用数(结果确认后更新)。 */
614
+ count: number;
615
+ /** 上次该调用的结果摘要(比较用)。 */
616
+ lastResult: string | undefined;
617
+ /** 本次调用等待结果确认。 */
618
+ waiting: boolean;
619
+ }
620
+ | undefined;
601
621
  /** 本回合已触发过 loop guard(防重复打断)。 */
602
622
  loopFired: boolean;
603
623
  /** 我们主动 cancel 过本回合(区分用户停止)。 */
@@ -620,6 +640,7 @@ const freshState = (): SessionState => ({
620
640
  lastTurn: undefined,
621
641
  pendingRecoveryAt: 0,
622
642
  shortRun: 0,
643
+ lastShortAt: 0,
623
644
  toolRun: undefined,
624
645
  loopFired: false,
625
646
  loopCancelled: false,
@@ -769,19 +790,35 @@ export class AutoContinueRunner {
769
790
  const state = this.state(frame.sessionId);
770
791
  state.lastTool = name;
771
792
  state.lastToolResult = 'pending'; // 已发起, 尚未见结果
772
- // loop guard 信号 2: 连续相同工具调用(工具调用 = 有进展, 重置短句信号)
793
+ // loop guard 信号 2: 同工具+同参数才可能是循环; 参数变化 = 有进展
794
+ // (工具调用本身也重置短句信号)。计数在结果确认后才推进。
773
795
  state.shortRun = 0;
774
- if (state.toolRun?.name === name) {
775
- state.toolRun.count += 1;
796
+ const key = `${name}\n${frame.event.data.arguments}`;
797
+ if (state.toolRun?.key === key) {
798
+ state.toolRun.waiting = true; // 结果到达时与上次结果比较
776
799
  } else {
777
- state.toolRun = { name, count: 1 };
800
+ state.toolRun = { key, count: 1, lastResult: undefined, waiting: false };
778
801
  }
779
- this.checkLoop(frame.sessionId, state);
780
802
  }
781
803
  } else if (frame.event.type === 'tool/result') {
782
804
  const state = this.state(frame.sessionId);
783
805
  if (state.lastToolResult === 'pending') {
784
- state.lastToolResult = toolResultFacts(frame.event.data);
806
+ const facts = toolResultFacts(frame.event.data);
807
+ state.lastToolResult = facts;
808
+ // 结果确认: 与上次相同 → 计数推进; 不同 → 有进展, 重置
809
+ const run = state.toolRun;
810
+ if (run !== undefined && run.waiting) {
811
+ run.waiting = false;
812
+ if (run.lastResult !== undefined && run.lastResult === facts.excerpt) {
813
+ run.count += 1;
814
+ this.checkLoop(frame.sessionId, state);
815
+ } else {
816
+ run.lastResult = facts.excerpt;
817
+ run.count = 1;
818
+ }
819
+ } else if (run !== undefined && !run.waiting) {
820
+ run.lastResult = facts.excerpt;
821
+ }
785
822
  }
786
823
  } else if (frame.event.type === 'assistant/message') {
787
824
  const state = this.state(frame.sessionId);
@@ -812,8 +849,9 @@ export class AutoContinueRunner {
812
849
  }
813
850
 
814
851
  /**
815
- * loop guard 信号 1(空转): 连续短句且期间无工具调用。
816
- * 短句 = 模型消息文本短于 loopShortChars; 长句或工具调用都会重置计数。
852
+ * loop guard 信号 1(空转): 时间窗内连续短句且期间无工具调用。
853
+ * 短句 = 模型消息文本短于 loopShortChars; 长句、工具调用、或短句间隔超过
854
+ * loopWindowMs(正常思考的短文本散布在长时间里)都会重置计数。
817
855
  */
818
856
  private onAssistantMessage(
819
857
  sessionId: SessionId,
@@ -823,9 +861,15 @@ export class AutoContinueRunner {
823
861
  if (!this.getConfig().loopGuard) return;
824
862
  const text = this.assistantText(event);
825
863
  if (text.trim().length < this.getConfig().loopShortChars) {
864
+ const now = Date.now();
865
+ if (now - state.lastShortAt > this.getConfig().loopWindowMs) {
866
+ state.shortRun = 0; // 超过时间窗: 上一次短句太久远, 不算连续
867
+ }
826
868
  state.shortRun += 1;
869
+ state.lastShortAt = now;
827
870
  } else {
828
871
  state.shortRun = 0; // 长句 = 有实际输出, 重置
872
+ state.lastShortAt = 0;
829
873
  }
830
874
  this.checkLoop(sessionId, state);
831
875
  }
@@ -840,7 +884,8 @@ export class AutoContinueRunner {
840
884
  this.log(`检测到空转循环 ${sessionId}: 连续 ${state.shortRun} 条短句且无工具调用`);
841
885
  void this.interruptLoop(sessionId, state);
842
886
  } else if (state.toolRun !== undefined && state.toolRun.count >= config.loopToolRepeat) {
843
- this.log(`检测到工具死循环 ${sessionId}: 「${state.toolRun.name}」连续 ${state.toolRun.count} 次`);
887
+ const toolName = state.toolRun.key.split('\n')[0] ?? '?';
888
+ this.log(`检测到工具死循环 ${sessionId}: 「${toolName}」连续 ${state.toolRun.count} 次(同参数同结果)`);
844
889
  void this.interruptLoop(sessionId, state);
845
890
  }
846
891
  }
@@ -882,6 +927,7 @@ export class AutoContinueRunner {
882
927
  state.lastToolResult = undefined;
883
928
  // loop guard 状态按回合重置
884
929
  state.shortRun = 0;
930
+ state.lastShortAt = 0;
885
931
  state.toolRun = undefined;
886
932
  state.loopFired = false;
887
933
  state.loopCancelled = false;
@@ -905,6 +951,7 @@ export class AutoContinueRunner {
905
951
  state.consecutive = 0;
906
952
  state.pendingRecoveryAt = 0;
907
953
  state.shortRun = 0;
954
+ state.lastShortAt = 0;
908
955
  state.toolRun = undefined;
909
956
  state.lastAttemptAt = 0;
910
957
  this.schedule(sessionId, 'loop:aborted');
@@ -57,10 +57,12 @@ export const zh = {
57
57
  'field.loopGuardHint': '检测运行中的回合空转: 连续短句且无工具调用, 或连续调用相同工具时, 自动取消并用循环提示文本重启回合。',
58
58
  'field.loopShortChars': '短句长度上限 (字符)',
59
59
  'field.loopShortCharsHint': '模型消息文本短于该值计为一条短句(空转信号)。',
60
+ 'field.loopWindowMs': '短句时间窗 (ms)',
61
+ 'field.loopWindowMsHint': '连续短句必须落在这个时间窗内; 正常思考的短文本散布在长时间里不会被误判。',
60
62
  'field.loopShortCount': '连续短句阈值',
61
- 'field.loopShortCountHint': '连续多少条短句且期间无工具调用时判定空转循环。',
62
- 'field.loopToolRepeat': '相同工具连续次数',
63
- 'field.loopToolRepeatHint': '连续调用同一工具多少次时判定死循环。',
63
+ 'field.loopShortCountHint': '时间窗内连续多少条短句且期间无工具调用时判定空转循环。',
64
+ 'field.loopToolRepeat': '同工具重复次数',
65
+ 'field.loopToolRepeatHint': '同工具+同参数+同结果的连续调用多少次时判定死循环; 参数或结果有变化视为有进展。',
64
66
  'field.loopText': '循环提示文本',
65
67
  'field.loopTextHint': '打断后重启回合时发送的文本, 支持 {tool} 占位符。',
66
68
  'stats.byCode': '按错误码统计',
@@ -143,10 +145,12 @@ export const en: Record<SettingsCardKey, string> = {
143
145
  '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.',
144
146
  'field.loopShortChars': 'Short-sentence max (chars)',
145
147
  'field.loopShortCharsHint': 'A model message shorter than this counts as a short sentence (spinning signal).',
148
+ 'field.loopWindowMs': 'Short-sentence window (ms)',
149
+ 'field.loopWindowMsHint': 'Consecutive short sentences must land inside this window; normal thinking spread over time is not misjudged.',
146
150
  'field.loopShortCount': 'Short-sentence threshold',
147
- 'field.loopShortCountHint': 'How many consecutive short sentences with no tool call trip the loop guard.',
151
+ 'field.loopShortCountHint': 'How many consecutive short sentences inside the window, with no tool call, trip the loop guard.',
148
152
  'field.loopToolRepeat': 'Same-tool repeat count',
149
- 'field.loopToolRepeatHint': 'How many consecutive identical tool calls trip the loop guard.',
153
+ 'field.loopToolRepeatHint': 'How many consecutive calls of the same tool with identical arguments and results trip the loop guard; a changed argument or result counts as progress.',
150
154
  'field.loopText': 'Loop text',
151
155
  'field.loopTextHint': 'Text sent after the loop guard restarts a turn; supports the {tool} placeholder.',
152
156
  'stats.byCode': 'By error code',
@@ -57,6 +57,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
57
57
  notify: CardFieldState;
58
58
  loopGuard: CardFieldState;
59
59
  loopShortChars: CardFieldState;
60
+ loopWindowMs: CardFieldState;
60
61
  loopShortCount: CardFieldState;
61
62
  loopToolRepeat: CardFieldState;
62
63
  loopText: CardFieldState;
@@ -101,6 +102,7 @@ export class AutoContinueSettingsCardController {
101
102
  booleanField('notify'),
102
103
  booleanField('loopGuard'),
103
104
  numberField('loopShortChars', 1),
105
+ numberField('loopWindowMs', 1000),
104
106
  numberField('loopShortCount', 2),
105
107
  numberField('loopToolRepeat', 2),
106
108
  textField('loopText'),
@@ -132,6 +134,7 @@ export class AutoContinueSettingsCardController {
132
134
  notify: this.form.field('notify'),
133
135
  loopGuard: this.form.field('loopGuard'),
134
136
  loopShortChars: this.form.field('loopShortChars'),
137
+ loopWindowMs: this.form.field('loopWindowMs'),
135
138
  loopShortCount: this.form.field('loopShortCount'),
136
139
  loopToolRepeat: this.form.field('loopToolRepeat'),
137
140
  loopText: this.form.field('loopText'),
@@ -617,6 +620,16 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
617
620
  onEdit={(text) => props.edit('loopShortChars', text)}
618
621
  onReset={() => props.resetField('loopShortChars')}
619
622
  />
623
+ <ValueField
624
+ id="auto-continue-loop-window-ms"
625
+ label={t('field.loopWindowMs')}
626
+ hint={t('field.loopWindowMsHint')}
627
+ numeric
628
+ {...shared}
629
+ {...state.loopWindowMs}
630
+ onEdit={(text) => props.edit('loopWindowMs', text)}
631
+ onReset={() => props.resetField('loopWindowMs')}
632
+ />
620
633
  <ValueField
621
634
  id="auto-continue-loop-short-count"
622
635
  label={t('field.loopShortCount')}
package/src/index.ts CHANGED
@@ -60,10 +60,12 @@ export const AutoContinueSchema = z.object({
60
60
  loopGuard: z.boolean().default(true),
61
61
  /** A model message shorter than this many chars counts as a short sentence (loop signal). */
62
62
  loopShortChars: z.natural().min(1).default(40),
63
- /** Consecutive short sentences with no tool call in between trip the loop guard. */
64
- loopShortCount: z.natural().min(2).default(8),
65
- /** Consecutive identical tool calls trip the loop guard. */
66
- loopToolRepeat: z.natural().min(2).default(4),
63
+ /** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
64
+ loopWindowMs: z.natural().min(1000).default(30000),
65
+ /** Consecutive short sentences trip the loop guard. */
66
+ loopShortCount: z.natural().min(2).default(12),
67
+ /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
68
+ loopToolRepeat: z.natural().min(2).default(5),
67
69
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
68
70
  loopText: z
69
71
  .string()