dsh-client-auto-continue 0.8.1 → 0.9.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
@@ -112,6 +112,7 @@ var DEFAULT_CONFIG = {
112
112
  freshMs: 15 * 60 * 1e3,
113
113
  verbose: true,
114
114
  classify: true,
115
+ retryableErrorPatterns: "",
115
116
  backoffFactor: 2,
116
117
  backoffMaxMs: 3e5,
117
118
  notify: false,
@@ -150,6 +151,7 @@ function resolveConfig(section) {
150
151
  freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
151
152
  verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
152
153
  classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
154
+ retryableErrorPatterns: typeof value.retryableErrorPatterns === "string" ? value.retryableErrorPatterns.trim() : DEFAULT_CONFIG.retryableErrorPatterns,
153
155
  backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
154
156
  backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
155
157
  notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
@@ -166,8 +168,10 @@ function resolveConfig(section) {
166
168
  function isNonHumanReason(kind) {
167
169
  return kind === "error" || kind === "interrupted" || kind === "max-tokens";
168
170
  }
169
- function isTransientFailure(failure) {
170
- const haystack = `${failure.code} ${failure.message}`.toLowerCase();
171
+ function isTransientFailure(failure, retryableErrorPatterns = "") {
172
+ const haystack = `${failure.code} ${failure.status ?? ""} ${failure.message}`.toLowerCase();
173
+ const explicitlyRetryable = retryableErrorPatterns.split(/\r?\n/).map((pattern) => pattern.trim().toLowerCase()).filter((pattern) => pattern !== "").some((pattern) => haystack.includes(pattern));
174
+ if (explicitlyRetryable) return true;
171
175
  const status = failure.status;
172
176
  if (status !== void 0 && (status === 401 || status === 403)) return false;
173
177
  const permanent = /auth|unauthor|forbidden|credential|api[_-]?key|permission/i.test(haystack) || /insufficient.*(balance|quota)|billing|payment|quota.*exceeded.*(?!retry)/i.test(haystack) || /model.*not[_-]?found|unknown[_-]?model|model[_-]?not[_-]?found|not.*support.*model/i.test(haystack) || /context.*(length|limit|overflow|exceed)|token.*limit|max.*context/i.test(haystack) || /invalid[_-]?request|bad[_-]?request/i.test(haystack);
@@ -564,7 +568,7 @@ ${event.data.arguments}`;
564
568
  // ---------- host 帧 ----------
565
569
  onTurnFailure(sessionId, reason, failure) {
566
570
  const config = this.getConfig();
567
- if (config.classify && !isTransientFailure(failure)) {
571
+ if (config.classify && !isTransientFailure(failure, config.retryableErrorPatterns)) {
568
572
  const summary = `${failure.code}${failure.status !== void 0 ? ` (HTTP ${failure.status})` : ""}`;
569
573
  this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
570
574
  this.bumpStat({ skipped: 1, code: failure.code });
@@ -913,6 +917,8 @@ var AutoContinueSchema = z2.object({
913
917
  verbose: z2.boolean().default(true),
914
918
  /** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
915
919
  classify: z2.boolean().default(true),
920
+ /** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
921
+ retryableErrorPatterns: z2.string().default(""),
916
922
  /** Cooldown multiplier per consecutive failure (adaptive backoff). */
917
923
  backoffFactor: z2.natural().min(1).default(2),
918
924
  /** Cap on the effective backoff interval (ms). */
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Snapshot-store bridge across the two public DSH client module layouts.
3
+ *
4
+ * DSH 0.1.2 moved the store engine from the dynamic
5
+ * `@deepseek-ai/dsh-client-runtime/client` row into the shell-seeded
6
+ * `@deepseek-ai/dsh-client-store` platform module. Keep the probe dynamic so
7
+ * esbuild does not turn both candidates into eager top-level requires: the
8
+ * loader must only resolve the module that exists in the running DSH cohort.
9
+ */
10
+ /** Writable observable snapshot used by the settings card. */
11
+ export interface SnapshotStore<T> {
12
+ getSnapshot(): T;
13
+ subscribe(listener: () => void): () => void;
14
+ update(mutator: (draft: T) => void): void;
15
+ set(next: T): void;
16
+ }
17
+ /** Settings state consumed by the staged form. */
18
+ export interface SettingsScopeSnapshot<T> {
19
+ status: 'loading' | 'ready' | 'unavailable';
20
+ value: T | undefined;
21
+ base: unknown;
22
+ user: unknown;
23
+ revision: number | undefined;
24
+ writable: boolean;
25
+ mode: 'host' | 'memory';
26
+ }
27
+ /** Stable subset shared by the legacy and DSH 0.1.2 settings scopes. */
28
+ export interface SettingsScope<T> {
29
+ getSnapshot(): SettingsScopeSnapshot<T>;
30
+ subscribe(listener: () => void): () => void;
31
+ set(field: string, value: unknown): Promise<void>;
32
+ unset(field: string): Promise<void>;
33
+ }
34
+ export declare const createSnapshotStore: <T>(init: T, options?: {
35
+ flush?: "raf" | "sync";
36
+ persist?: {
37
+ name: string;
38
+ };
39
+ }) => SnapshotStore<T>;
@@ -9,7 +9,7 @@
9
9
  * action endpoint,
10
10
  * - feeds the card's stats / paused-sessions panels from the bridge state.
11
11
  */
12
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
12
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
13
13
  import { type SettingsCardKey } from './locales.ts';
14
14
  declare module '@deepseek-ai/dsh-client-ui-slots' {
15
15
  interface LocaleNamespaceMap {
@@ -35,6 +35,8 @@ export declare const zh: {
35
35
  'field.verboseHint': string;
36
36
  'field.classify': string;
37
37
  'field.classifyHint': string;
38
+ 'field.retryableErrorPatterns': string;
39
+ 'field.retryableErrorPatternsHint': string;
38
40
  'field.backoffFactor': string;
39
41
  'field.backoffFactorHint': string;
40
42
  'field.backoffMaxMs': string;
@@ -1,6 +1,6 @@
1
- import { type SettingsScope, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
2
1
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
2
  import { type AutoContinueSettings } from './engine.ts';
3
+ import { type SettingsScope, type SnapshotStore } from './dsh-store-compat.ts';
4
4
  import { type CardActions, type CardFieldState, type CardShell } from './settings-form.ts';
5
5
  /** What the auto-continue card renders. */
6
6
  export interface AutoContinueSettingsCardState extends CardShell {
@@ -18,6 +18,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
18
18
  freshMs: CardFieldState;
19
19
  verbose: CardFieldState;
20
20
  classify: CardFieldState;
21
+ retryableErrorPatterns: CardFieldState;
21
22
  backoffFactor: CardFieldState;
22
23
  backoffMaxMs: CardFieldState;
23
24
  notify: CardFieldState;
@@ -10,7 +10,7 @@
10
10
  * default — and whether the user layer carries it (presence, not value
11
11
  * equality, marks an override).
12
12
  */
13
- import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
13
+ import type { SettingsScope, SnapshotStore } from './dsh-store-compat.ts';
14
14
  /** The write one field's staged text performs when the card is saved. */
15
15
  export type FieldWrite = {
16
16
  kind: 'set';
@@ -41,6 +41,8 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
41
41
  verbose: z<boolean, boolean>;
42
42
  /** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
43
43
  classify: z<boolean, boolean>;
44
+ /** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
45
+ retryableErrorPatterns: z<string, string>;
44
46
  /** Cooldown multiplier per consecutive failure (adaptive backoff). */
45
47
  backoffFactor: z<number, number>;
46
48
  /** Cap on the effective backoff interval (ms). */
@@ -90,6 +92,8 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
90
92
  verbose: z<boolean, boolean>;
91
93
  /** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
92
94
  classify: z<boolean, boolean>;
95
+ /** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
96
+ retryableErrorPatterns: z<string, string>;
93
97
  /** Cooldown multiplier per consecutive failure (adaptive backoff). */
94
98
  backoffFactor: z<number, number>;
95
99
  /** Cap on the effective backoff interval (ms). */
@@ -33,6 +33,8 @@ export interface AutoContinueSettings {
33
33
  verbose?: boolean;
34
34
  /** Classify failures: auto-continue transient errors only; permanent ones (auth/balance/model) are skipped and notified. */
35
35
  classify?: boolean;
36
+ /** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
37
+ retryableErrorPatterns?: string;
36
38
  /** Cooldown multiplier per consecutive failure (adaptive backoff). */
37
39
  backoffFactor?: number;
38
40
  /** Cap on the effective backoff interval (ms). */
@@ -81,10 +83,11 @@ export interface FailureFacts {
81
83
  }
82
84
  /**
83
85
  * 错误分类: 该失败是否值得自动继续。
86
+ * 用户填写的 provider 专属文本片段优先覆盖内置结果; 未命中时,
84
87
  * 永久性失败(认证/余额/模型不存在/上下文超限等)重试也不会成功, 应跳过并通知用户;
85
88
  * 其余(网络、超时、5xx、429 等)视为临时性失败, 允许自动恢复。
86
89
  */
87
- export declare function isTransientFailure(failure: FailureFacts): boolean;
90
+ export declare function isTransientFailure(failure: FailureFacts, retryableErrorPatterns?: string): boolean;
88
91
  /**
89
92
  * host/agent-error 消息分类: 仅明确属于网络/传输类的临时错误才自动继续。
90
93
  * 其余(序列化失败、配置/宿主内部错误等)视为永久性——重试无益, 且用户停止导致的
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.8.1",
4
+ "version": "0.9.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -36,7 +36,6 @@
36
36
  "platform": "web",
37
37
  "inject": [
38
38
  "@deepseek-ai/dsh-client-connection",
39
- "@deepseek-ai/dsh-client-runtime",
40
39
  "@deepseek-ai/dsh-client-locale",
41
40
  "@deepseek-ai/dsh-client-ui-settings",
42
41
  "@deepseek-ai/dsh-client-ui-settings-plugins"
@@ -47,7 +46,7 @@
47
46
  "build": "node build.mjs && tsc -p tsconfig.build.json",
48
47
  "watch": "node build.mjs --watch",
49
48
  "typecheck": "tsc --noEmit",
50
- "test": "node tests/simulate-host.mjs",
49
+ "test": "node tests/simulate-host.mjs && node tests/simulate-client-loader.mjs",
51
50
  "prepack": "npm run build"
52
51
  },
53
52
  "keywords": [
@@ -70,7 +69,6 @@
70
69
  "@deepseek-ai/cordis": "^4.0.1",
71
70
  "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
72
71
  "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
73
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
74
72
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
75
73
  "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.7",
76
74
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Snapshot-store bridge across the two public DSH client module layouts.
3
+ *
4
+ * DSH 0.1.2 moved the store engine from the dynamic
5
+ * `@deepseek-ai/dsh-client-runtime/client` row into the shell-seeded
6
+ * `@deepseek-ai/dsh-client-store` platform module. Keep the probe dynamic so
7
+ * esbuild does not turn both candidates into eager top-level requires: the
8
+ * loader must only resolve the module that exists in the running DSH cohort.
9
+ */
10
+
11
+ /** Writable observable snapshot used by the settings card. */
12
+ export interface SnapshotStore<T> {
13
+ getSnapshot(): T;
14
+ subscribe(listener: () => void): () => void;
15
+ update(mutator: (draft: T) => void): void;
16
+ set(next: T): void;
17
+ }
18
+
19
+ /** Settings state consumed by the staged form. */
20
+ export interface SettingsScopeSnapshot<T> {
21
+ status: 'loading' | 'ready' | 'unavailable';
22
+ value: T | undefined;
23
+ base: unknown;
24
+ user: unknown;
25
+ revision: number | undefined;
26
+ writable: boolean;
27
+ mode: 'host' | 'memory';
28
+ }
29
+
30
+ /** Stable subset shared by the legacy and DSH 0.1.2 settings scopes. */
31
+ export interface SettingsScope<T> {
32
+ getSnapshot(): SettingsScopeSnapshot<T>;
33
+ subscribe(listener: () => void): () => void;
34
+ set(field: string, value: unknown): Promise<void>;
35
+ unset(field: string): Promise<void>;
36
+ }
37
+
38
+ interface SnapshotStoreModule {
39
+ createSnapshotStore<T>(
40
+ init: T,
41
+ options?: { flush?: 'raf' | 'sync'; persist?: { name: string } },
42
+ ): SnapshotStore<T>;
43
+ }
44
+
45
+ function resolveSnapshotStore(): SnapshotStoreModule {
46
+ // String assembly is intentional: it preserves the lazy try/fallback in the
47
+ // emitted client bundle instead of letting the bundler resolve both names.
48
+ const current = ['@deepseek-ai/dsh-client', '-store'].join('');
49
+ const legacy = ['@deepseek-ai/dsh-client-runtime', '/client'].join('');
50
+ try {
51
+ return require(current) as SnapshotStoreModule;
52
+ } catch {
53
+ return require(legacy) as SnapshotStoreModule;
54
+ }
55
+ }
56
+
57
+ export const { createSnapshotStore } = resolveSnapshotStore();
@@ -9,7 +9,7 @@
9
9
  * action endpoint,
10
10
  * - feeds the card's stats / paused-sessions panels from the bridge state.
11
11
  */
12
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
12
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
13
13
  // Type-only: pulls the locale plugin's Context merge (ctx.locale).
14
14
  import type {} from '@deepseek-ai/dsh-client-locale/client';
15
15
  // Type-only: pulls the settings-surface SlotMap merge and ctx.settingsScope.
@@ -36,6 +36,8 @@ export const zh = {
36
36
  'field.verboseHint': '在浏览器控制台输出 [auto-continue] 日志。',
37
37
  'field.classify': '错误分类',
38
38
  'field.classifyHint': '仅自动恢复临时性错误(网络/超时/5xx 等); 认证/余额/模型不存在等永久性错误跳过并通知。',
39
+ 'field.retryableErrorPatterns': '自定义可恢复错误',
40
+ 'field.retryableErrorPatternsHint': '每行一个大小写不敏感的普通文本片段; 命中错误码、HTTP 状态或消息时覆盖内置分类。请只填 provider 稳定且足够具体的文案, 过宽会重复请求。',
39
41
  'field.backoffFactor': '退避系数',
40
42
  'field.backoffFactorHint': '连续失败时冷却间隔的倍率(如 2 表示 20s→40s→80s 递增)。',
41
43
  'field.backoffMaxMs': '最大退避间隔 (ms)',
@@ -122,6 +124,8 @@ export const en: Record<SettingsCardKey, string> = {
122
124
  'field.verboseHint': 'Log [auto-continue] lines to the browser console.',
123
125
  'field.classify': 'Classify errors',
124
126
  'field.classifyHint': 'Auto-resume transient failures only (network/timeout/5xx…); auth, balance and model errors are skipped and notified.',
127
+ 'field.retryableErrorPatterns': 'Custom retryable errors',
128
+ 'field.retryableErrorPatternsHint': 'One case-insensitive literal per line. A match in the error code, HTTP status, or message overrides built-in classification. Use only stable, provider-specific text; broad matches can repeat requests.',
125
129
  'field.backoffFactor': 'Backoff factor',
126
130
  'field.backoffFactorHint': 'Cooldown multiplier per consecutive failure (2 = 20s→40s→80s…).',
127
131
  'field.backoffMaxMs': 'Max backoff (ms)',
@@ -8,9 +8,9 @@
8
8
  * tokens so the card follows the active theme.
9
9
  */
10
10
  import { useEffect, useState, type ReactNode } from 'react';
11
- import { createSnapshotStore, type SettingsScope, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
12
11
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
13
12
  import { DEFAULT_CONFIG, type AutoContinueSettings } from './engine.ts';
13
+ import { createSnapshotStore, type SettingsScope, type SnapshotStore } from './dsh-store-compat.ts';
14
14
  import {
15
15
  pausedSessions,
16
16
  readTodayStats,
@@ -50,6 +50,7 @@ export interface AutoContinueSettingsCardState extends CardShell {
50
50
  freshMs: CardFieldState;
51
51
  verbose: CardFieldState;
52
52
  classify: CardFieldState;
53
+ retryableErrorPatterns: CardFieldState;
53
54
  backoffFactor: CardFieldState;
54
55
  backoffMaxMs: CardFieldState;
55
56
  notify: CardFieldState;
@@ -94,6 +95,7 @@ export class AutoContinueSettingsCardController {
94
95
  numberField('freshMs', 0),
95
96
  booleanField('verbose'),
96
97
  booleanField('classify'),
98
+ textField('retryableErrorPatterns'),
97
99
  numberField('backoffFactor', 1),
98
100
  numberField('backoffMaxMs', 0),
99
101
  booleanField('notify'),
@@ -125,6 +127,7 @@ export class AutoContinueSettingsCardController {
125
127
  freshMs: this.form.field('freshMs'),
126
128
  verbose: this.form.field('verbose'),
127
129
  classify: this.form.field('classify'),
130
+ retryableErrorPatterns: this.form.field('retryableErrorPatterns'),
128
131
  backoffFactor: this.form.field('backoffFactor'),
129
132
  backoffMaxMs: this.form.field('backoffMaxMs'),
130
133
  notify: this.form.field('notify'),
@@ -230,7 +233,8 @@ interface FieldProps {
230
233
  }
231
234
 
232
235
  /** A staged value field; `numeric` only hints the keypad, which drafts a field accepts is decided by its spec. */
233
- function ValueField(props: FieldProps & { numeric?: boolean; placeholder?: string }) {
236
+ function ValueField(props: FieldProps & { numeric?: boolean; multiline?: boolean; placeholder?: string }) {
237
+ const className = props.invalid ? 'dshAcInput dshAcInputInvalid' : 'dshAcInput';
234
238
  return (
235
239
  <div className="dshAcField">
236
240
  <div className="dshAcHead">
@@ -244,17 +248,30 @@ function ValueField(props: FieldProps & { numeric?: boolean; placeholder?: strin
244
248
  </span>
245
249
  ) : null}
246
250
  </div>
247
- <input
248
- id={props.id}
249
- className={props.invalid ? 'dshAcInput dshAcInputInvalid' : 'dshAcInput'}
250
- type="text"
251
- inputMode={props.numeric === true ? 'numeric' : undefined}
252
- aria-invalid={props.invalid || undefined}
253
- value={props.text}
254
- placeholder={props.placeholder ?? ''}
255
- disabled={props.disabled}
256
- onChange={(event) => props.onEdit(event.target.value)}
257
- />
251
+ {props.multiline === true ? (
252
+ <textarea
253
+ id={props.id}
254
+ className={`${className} dshAcTextArea`}
255
+ aria-invalid={props.invalid || undefined}
256
+ value={props.text}
257
+ placeholder={props.placeholder ?? ''}
258
+ disabled={props.disabled}
259
+ rows={4}
260
+ onChange={(event) => props.onEdit(event.target.value)}
261
+ />
262
+ ) : (
263
+ <input
264
+ id={props.id}
265
+ className={className}
266
+ type="text"
267
+ inputMode={props.numeric === true ? 'numeric' : undefined}
268
+ aria-invalid={props.invalid || undefined}
269
+ value={props.text}
270
+ placeholder={props.placeholder ?? ''}
271
+ disabled={props.disabled}
272
+ onChange={(event) => props.onEdit(event.target.value)}
273
+ />
274
+ )}
258
275
  <p className={props.invalid ? 'dshAcInvalid' : 'dshAcHint'}>
259
276
  {props.invalid ? props.t('chrome.invalidNumber') : props.hint}
260
277
  </p>
@@ -554,6 +571,17 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
554
571
  onEdit={(text) => props.edit('classify', text)}
555
572
  onReset={() => props.resetField('classify')}
556
573
  />
574
+ <ValueField
575
+ id="auto-continue-retryable-error-patterns"
576
+ label={t('field.retryableErrorPatterns')}
577
+ hint={t('field.retryableErrorPatternsHint')}
578
+ multiline
579
+ {...shared}
580
+ {...state.retryableErrorPatterns}
581
+ onEdit={(text) => props.edit('retryableErrorPatterns', text)}
582
+ placeholder="Upstream rejected the request as invalid"
583
+ onReset={() => props.resetField('retryableErrorPatterns')}
584
+ />
557
585
  <ValueField
558
586
  id="auto-continue-backoff-factor"
559
587
  label={t('field.backoffFactor')}
@@ -10,7 +10,7 @@
10
10
  * default — and whether the user layer carries it (presence, not value
11
11
  * equality, marks an override).
12
12
  */
13
- import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
13
+ import type { SettingsScope, SnapshotStore } from './dsh-store-compat.ts';
14
14
 
15
15
  /** The write one field's staged text performs when the card is saved. */
16
16
  export type FieldWrite = { kind: 'set'; value: unknown } | { kind: 'clear' };
@@ -117,6 +117,7 @@ const css = `
117
117
  .dshAcInput:focus-visible { border-color: var(--dsw-alias-brand-primary); outline: none; }
118
118
  .dshAcInput:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
119
119
  .dshAcInputInvalid { border-color: var(--dsw-alias-label-error); }
120
+ .dshAcTextArea { box-sizing: border-box; height: auto; min-height: 84px; padding: 8px 12px; resize: vertical; }
120
121
  .dshAcSelect {
121
122
  border: 1px solid var(--dsw-alias-border-l2);
122
123
  background: var(--dsw-alias-bg-layer-3);
@@ -459,7 +459,7 @@ export class AutoContinueRunner {
459
459
 
460
460
  private onTurnFailure(sessionId: SessionId, reason: string, failure: FailureFacts): void {
461
461
  const config = this.getConfig();
462
- if (config.classify && !isTransientFailure(failure)) {
462
+ if (config.classify && !isTransientFailure(failure, config.retryableErrorPatterns)) {
463
463
  const summary = `${failure.code}${failure.status !== undefined ? ` (HTTP ${failure.status})` : ''}`;
464
464
  this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
465
465
  this.bumpStat({ skipped: 1, code: failure.code });
package/src/index.ts CHANGED
@@ -56,6 +56,8 @@ export const AutoContinueSchema = z.object({
56
56
  verbose: z.boolean().default(true),
57
57
  /** Classify failures: auto-continue transient errors only; permanent ones are skipped and notified. */
58
58
  classify: z.boolean().default(true),
59
+ /** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
60
+ retryableErrorPatterns: z.string().default(''),
59
61
  /** Cooldown multiplier per consecutive failure (adaptive backoff). */
60
62
  backoffFactor: z.natural().min(1).default(2),
61
63
  /** Cap on the effective backoff interval (ms). */
@@ -34,6 +34,8 @@ export interface AutoContinueSettings {
34
34
  verbose?: boolean;
35
35
  /** Classify failures: auto-continue transient errors only; permanent ones (auth/balance/model) are skipped and notified. */
36
36
  classify?: boolean;
37
+ /** Provider-specific message/code/status fragments that explicitly count as retryable, one literal per line. */
38
+ retryableErrorPatterns?: string;
37
39
  /** Cooldown multiplier per consecutive failure (adaptive backoff). */
38
40
  backoffFactor?: number;
39
41
  /** Cap on the effective backoff interval (ms). */
@@ -76,6 +78,7 @@ export const DEFAULT_CONFIG: AutoContinueConfig = {
76
78
  freshMs: 15 * 60 * 1000,
77
79
  verbose: true,
78
80
  classify: true,
81
+ retryableErrorPatterns: '',
79
82
  backoffFactor: 2,
80
83
  backoffMaxMs: 300000,
81
84
  notify: false,
@@ -130,6 +133,10 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
130
133
  freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
131
134
  verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
132
135
  classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
136
+ retryableErrorPatterns:
137
+ typeof value.retryableErrorPatterns === 'string'
138
+ ? value.retryableErrorPatterns.trim()
139
+ : DEFAULT_CONFIG.retryableErrorPatterns,
133
140
  backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
134
141
  backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
135
142
  notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
@@ -171,11 +178,18 @@ export interface FailureFacts {
171
178
 
172
179
  /**
173
180
  * 错误分类: 该失败是否值得自动继续。
181
+ * 用户填写的 provider 专属文本片段优先覆盖内置结果; 未命中时,
174
182
  * 永久性失败(认证/余额/模型不存在/上下文超限等)重试也不会成功, 应跳过并通知用户;
175
183
  * 其余(网络、超时、5xx、429 等)视为临时性失败, 允许自动恢复。
176
184
  */
177
- export function isTransientFailure(failure: FailureFacts): boolean {
178
- const haystack = `${failure.code} ${failure.message}`.toLowerCase();
185
+ export function isTransientFailure(failure: FailureFacts, retryableErrorPatterns = ''): boolean {
186
+ const haystack = `${failure.code} ${failure.status ?? ''} ${failure.message}`.toLowerCase();
187
+ const explicitlyRetryable = retryableErrorPatterns
188
+ .split(/\r?\n/)
189
+ .map((pattern) => pattern.trim().toLowerCase())
190
+ .filter((pattern) => pattern !== '')
191
+ .some((pattern) => haystack.includes(pattern));
192
+ if (explicitlyRetryable) return true;
179
193
  const status = failure.status;
180
194
  if (status !== undefined && (status === 401 || status === 403)) return false;
181
195
  const permanent =
@@ -456,4 +470,3 @@ export function isOurEcho(state: SessionState, event: SessionEvent): boolean {
456
470
  .join('');
457
471
  return text === state.lastSentText;
458
472
  }
459
-