dsh-client-auto-continue 0.7.2 → 0.7.3

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
@@ -49,6 +49,8 @@ var AutoContinueSchema = z.object({
49
49
  loopWindowMs: z.natural().min(1e3).default(3e4),
50
50
  /** Consecutive short sentences trip the loop guard. */
51
51
  loopShortCount: z.natural().min(2).default(12),
52
+ /** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
53
+ loopRepeatText: z.natural().min(2).default(4),
52
54
  /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
53
55
  loopToolRepeat: z.natural().min(2).default(5),
54
56
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
@@ -61,6 +61,8 @@ export interface AutoContinueSettings {
61
61
  loopShortCount?: number;
62
62
  /** Consecutive identical tool calls with identical arguments AND identical results trip the loop guard. */
63
63
  loopToolRepeat?: number;
64
+ /** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
65
+ loopRepeatText?: number;
64
66
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
65
67
  loopText?: string;
66
68
  }
@@ -60,6 +60,8 @@ export declare const zh: {
60
60
  'field.loopWindowMsHint': string;
61
61
  'field.loopShortCount': string;
62
62
  'field.loopShortCountHint': string;
63
+ 'field.loopRepeatText': string;
64
+ 'field.loopRepeatTextHint': string;
63
65
  'field.loopToolRepeat': string;
64
66
  'field.loopToolRepeatHint': string;
65
67
  'field.loopText': string;
@@ -27,6 +27,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
27
27
  loopShortChars: CardFieldState;
28
28
  loopWindowMs: CardFieldState;
29
29
  loopShortCount: CardFieldState;
30
+ loopRepeatText: CardFieldState;
30
31
  loopToolRepeat: CardFieldState;
31
32
  loopText: CardFieldState;
32
33
  }
@@ -55,6 +55,8 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
55
55
  loopWindowMs: z<number, number>;
56
56
  /** Consecutive short sentences trip the loop guard. */
57
57
  loopShortCount: z<number, number>;
58
+ /** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
59
+ loopRepeatText: z<number, number>;
58
60
  /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
59
61
  loopToolRepeat: z<number, number>;
60
62
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
@@ -106,6 +108,8 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
106
108
  loopWindowMs: z<number, number>;
107
109
  /** Consecutive short sentences trip the loop guard. */
108
110
  loopShortCount: z<number, number>;
111
+ /** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
112
+ loopRepeatText: z<number, number>;
109
113
  /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
110
114
  loopToolRepeat: z<number, number>;
111
115
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
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.2",
4
+ "version": "0.7.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -70,6 +70,8 @@ export interface AutoContinueSettings {
70
70
  loopShortCount?: number;
71
71
  /** Consecutive identical tool calls with identical arguments AND identical results trip the loop guard. */
72
72
  loopToolRepeat?: number;
73
+ /** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
74
+ loopRepeatText?: number;
73
75
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
74
76
  loopText?: string;
75
77
  }
@@ -102,6 +104,7 @@ export const DEFAULT_CONFIG: AutoContinueConfig = {
102
104
  loopShortChars: 40,
103
105
  loopWindowMs: 30000,
104
106
  loopShortCount: 12,
107
+ loopRepeatText: 4,
105
108
  loopToolRepeat: 5,
106
109
  loopText: '(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)',
107
110
  };
@@ -157,6 +160,7 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
157
160
  loopShortChars: Math.max(1, numberOr(value.loopShortChars, DEFAULT_CONFIG.loopShortChars)),
158
161
  loopWindowMs: Math.max(1000, numberOr(value.loopWindowMs, DEFAULT_CONFIG.loopWindowMs)),
159
162
  loopShortCount: Math.max(2, numberOr(value.loopShortCount, DEFAULT_CONFIG.loopShortCount)),
163
+ loopRepeatText: Math.max(2, numberOr(value.loopRepeatText, DEFAULT_CONFIG.loopRepeatText)),
160
164
  loopToolRepeat: Math.max(2, numberOr(value.loopToolRepeat, DEFAULT_CONFIG.loopToolRepeat)),
161
165
  loopText:
162
166
  typeof value.loopText === 'string' && value.loopText.trim() !== ''
@@ -602,6 +606,10 @@ interface SessionState {
602
606
  shortRun: number;
603
607
  /** 最后一条短句的时间(时间窗判定用)。 */
604
608
  lastShortAt: number;
609
+ /** 最后一条模型消息的文本(相同文本重复判定用)。 */
610
+ lastAssistantText: string;
611
+ /** 连续相同文本消息数(最强空转信号, 不限长度)。 */
612
+ sameTextRun: number;
605
613
  /**
606
614
  * 工具重复信号(loop guard 信号 2: 死循环)。
607
615
  * 只有「同工具 + 同参数 + 同结果」的连续调用才累计; 参数或结果有变化视为有进展, 计数重置。
@@ -641,6 +649,8 @@ const freshState = (): SessionState => ({
641
649
  pendingRecoveryAt: 0,
642
650
  shortRun: 0,
643
651
  lastShortAt: 0,
652
+ lastAssistantText: '',
653
+ sameTextRun: 0,
644
654
  toolRun: undefined,
645
655
  loopFired: false,
646
656
  loopCancelled: false,
@@ -860,7 +870,17 @@ export class AutoContinueRunner {
860
870
  ): void {
861
871
  if (!this.getConfig().loopGuard) return;
862
872
  const text = this.assistantText(event);
863
- if (text.trim().length < this.getConfig().loopShortChars) {
873
+ const trimmed = text.trim();
874
+ // 相同文本重复(不限长度): 模型反复输出完全相同的消息是最强的循环信号,
875
+ // 例如 "Let me test variants of the regex..." 连续 7 遍
876
+ if (trimmed !== '' && trimmed === state.lastAssistantText) {
877
+ state.sameTextRun += 1;
878
+ } else {
879
+ state.lastAssistantText = trimmed;
880
+ state.sameTextRun = 1;
881
+ }
882
+ // 短句计数(长度 < loopShortChars 且落在时间窗内): 空转信号
883
+ if (trimmed.length < this.getConfig().loopShortChars) {
864
884
  const now = Date.now();
865
885
  if (now - state.lastShortAt > this.getConfig().loopWindowMs) {
866
886
  state.shortRun = 0; // 超过时间窗: 上一次短句太久远, 不算连续
@@ -880,7 +900,10 @@ export class AutoContinueRunner {
880
900
  if (state.loopFired) return;
881
901
  if (!state.running) return; // 只干预运行中的回合
882
902
  const config = this.getConfig();
883
- if (state.shortRun >= config.loopShortCount) {
903
+ if (state.sameTextRun >= config.loopRepeatText) {
904
+ this.log(`检测到空转循环 ${sessionId}: 连续 ${state.sameTextRun} 条相同消息`);
905
+ void this.interruptLoop(sessionId, state);
906
+ } else if (state.shortRun >= config.loopShortCount) {
884
907
  this.log(`检测到空转循环 ${sessionId}: 连续 ${state.shortRun} 条短句且无工具调用`);
885
908
  void this.interruptLoop(sessionId, state);
886
909
  } else if (state.toolRun !== undefined && state.toolRun.count >= config.loopToolRepeat) {
@@ -928,6 +951,8 @@ export class AutoContinueRunner {
928
951
  // loop guard 状态按回合重置
929
952
  state.shortRun = 0;
930
953
  state.lastShortAt = 0;
954
+ state.lastAssistantText = '';
955
+ state.sameTextRun = 0;
931
956
  state.toolRun = undefined;
932
957
  state.loopFired = false;
933
958
  state.loopCancelled = false;
@@ -952,6 +977,8 @@ export class AutoContinueRunner {
952
977
  state.pendingRecoveryAt = 0;
953
978
  state.shortRun = 0;
954
979
  state.lastShortAt = 0;
980
+ state.lastAssistantText = '';
981
+ state.sameTextRun = 0;
955
982
  state.toolRun = undefined;
956
983
  state.lastAttemptAt = 0;
957
984
  this.schedule(sessionId, 'loop:aborted');
@@ -61,6 +61,8 @@ export const zh = {
61
61
  'field.loopWindowMsHint': '连续短句必须落在这个时间窗内; 正常思考的短文本散布在长时间里不会被误判。',
62
62
  'field.loopShortCount': '连续短句阈值',
63
63
  'field.loopShortCountHint': '时间窗内连续多少条短句且期间无工具调用时判定空转循环。',
64
+ 'field.loopRepeatText': '相同消息重复次数',
65
+ 'field.loopRepeatTextHint': '连续输出多少条完全相同的消息时判定空转(最强信号, 不限长度, 如模型反复说同一句话)。',
64
66
  'field.loopToolRepeat': '同工具重复次数',
65
67
  'field.loopToolRepeatHint': '同工具+同参数+同结果的连续调用多少次时判定死循环; 参数或结果有变化视为有进展。',
66
68
  'field.loopText': '循环提示文本',
@@ -149,6 +151,8 @@ export const en: Record<SettingsCardKey, string> = {
149
151
  'field.loopWindowMsHint': 'Consecutive short sentences must land inside this window; normal thinking spread over time is not misjudged.',
150
152
  'field.loopShortCount': 'Short-sentence threshold',
151
153
  'field.loopShortCountHint': 'How many consecutive short sentences inside the window, with no tool call, trip the loop guard.',
154
+ 'field.loopRepeatText': 'Identical message count',
155
+ 'field.loopRepeatTextHint': 'How many consecutive identical messages trip the guard (strongest signal, any length, e.g. the model repeating the same line).',
152
156
  'field.loopToolRepeat': 'Same-tool repeat count',
153
157
  '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.',
154
158
  'field.loopText': 'Loop text',
@@ -59,6 +59,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
59
59
  loopShortChars: CardFieldState;
60
60
  loopWindowMs: CardFieldState;
61
61
  loopShortCount: CardFieldState;
62
+ loopRepeatText: CardFieldState;
62
63
  loopToolRepeat: CardFieldState;
63
64
  loopText: CardFieldState;
64
65
  }
@@ -104,6 +105,7 @@ export class AutoContinueSettingsCardController {
104
105
  numberField('loopShortChars', 1),
105
106
  numberField('loopWindowMs', 1000),
106
107
  numberField('loopShortCount', 2),
108
+ numberField('loopRepeatText', 2),
107
109
  numberField('loopToolRepeat', 2),
108
110
  textField('loopText'),
109
111
  ]);
@@ -136,6 +138,7 @@ export class AutoContinueSettingsCardController {
136
138
  loopShortChars: this.form.field('loopShortChars'),
137
139
  loopWindowMs: this.form.field('loopWindowMs'),
138
140
  loopShortCount: this.form.field('loopShortCount'),
141
+ loopRepeatText: this.form.field('loopRepeatText'),
139
142
  loopToolRepeat: this.form.field('loopToolRepeat'),
140
143
  loopText: this.form.field('loopText'),
141
144
  };
@@ -650,6 +653,16 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
650
653
  onEdit={(text) => props.edit('loopToolRepeat', text)}
651
654
  onReset={() => props.resetField('loopToolRepeat')}
652
655
  />
656
+ <ValueField
657
+ id="auto-continue-loop-repeat-text"
658
+ label={t('field.loopRepeatText')}
659
+ hint={t('field.loopRepeatTextHint')}
660
+ numeric
661
+ {...shared}
662
+ {...state.loopRepeatText}
663
+ onEdit={(text) => props.edit('loopRepeatText', text)}
664
+ onReset={() => props.resetField('loopRepeatText')}
665
+ />
653
666
  <ValueField
654
667
  id="auto-continue-loop-text"
655
668
  label={t('field.loopText')}
package/src/index.ts CHANGED
@@ -64,6 +64,8 @@ export const AutoContinueSchema = z.object({
64
64
  loopWindowMs: z.natural().min(1000).default(30000),
65
65
  /** Consecutive short sentences trip the loop guard. */
66
66
  loopShortCount: z.natural().min(2).default(12),
67
+ /** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
68
+ loopRepeatText: z.natural().min(2).default(4),
67
69
  /** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
68
70
  loopToolRepeat: z.natural().min(2).default(5),
69
71
  /** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */