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