Skip to content

fix(minimax-bot): 回合重试从 2 次延长到 10 档(仍有上限),400/404/422 不重试 - #49

Open
Hylouis233 wants to merge 6 commits into
MiniMax-AI:mainfrom
Hylouis233:feat/minimax-bot-retry-window
Open

Hylouis233 wants to merge 6 commits into
MiniMax-AI:mainfrom
Hylouis233:feat/minimax-bot-retry-window

Conversation

@Hylouis233

@Hylouis233 Hylouis233 commented Oct 20, 2026 •

Copy link
Copy Markdown

fix(minimax-bot): 回合重试从 2 次延长到 10 档(仍有上限),400/404/422 不重试

要解决的问题:一次 30 秒的服务端抖动就让 Bot 死掉

上游 0.7.4 的重试窗口是 TURN_RETRY_DELAYS_MS = [8000, 20000] ——
只重试 2 次(等 8 秒、等 20 秒),然后就放弃。

而 Host 的 toLLMRetryDecision() 对通用 5xx 故意 fail-closed:
LLM_UPSTREAM_ERROR(50113)是「400 及以上的兜底桶」,
把 HTTP 400/404 这种重试无意义的错误和 HTTP 500 这种暂时性错误混在一起,
于是分类器只能整体判成不可重试。

两条合起来的后果:
一次持续 30 秒的服务端抖动(Internal server error (500) / 502 Bad Gateway /
net::ERR_NETWORK_CHANGED / 限流 / 模型过载)就会让回合第一次失败就死掉,
只能靠人手点一下才回来。Bot 团队的可用性因此被一次瞬时故障决定。

复现场景

  1. 让 MiniMax Bot 团队里任何一个 bot 收到一条任务;
  2. 在它请求模型时让上游返回一次 500(或断网、限流、网关 502);
  3. 观察:该回合只尝试了 3 次(首次 + 2 次重试,共约 28 秒)就返回 error,
    bot 停在原地,需要人工再点一次。

改动

1. 重试窗口从 2 档延长到 10 档(仍然有上限)

8s → 20s → 40s → 60s × 7

总窗口约 8 分钟,足以自愈绝大多数瞬时抖动。

  • 有上限:第 10 次之后把原始错误原样暴露给用户,不会悄悄重试到天荒地老。
  • 窗口数组仍是唯一的旋钮:注入 retryDelaysMs: [] 依然表示「完全不重试」,
    既有语义与既有测试一字未改(engine-reliability.test.mjs 51/51 通过)。

2. 400 / 404 / 422 不重试

重试它们只是把同一次拒绝重复十遍。Host 自己的注释说得很清楚:
would turn an HTTP 400 or 404 into ten identical attempts。

3. 错误文本匹配收窄

只认明确的服务端 / 网络故障形态:

net::ERR_NETWORK_CHANGED / ERR_NETWORK / ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE
socket hang up / internal server error / bad gateway|gateway timeout|service unavailable
请求处理超时 / unknown error, 520 / overloaded|rate limit|too many requests|model is busy
HTTP 500 / status 502 / code=503(要带上下文,不认裸数字)
  • 不认裸数字 ⇒「用了 400 tokens」「第 404 步」这类正常消息不会被误判成错误
  • 不含 400/404/422 ⇒ 上面那条不重试的规则
  • 判定「这次失败值不值得重试」的第一道依据始终是 Host 自己给的
    result.error.retryable 标志;文本匹配只是补上 Host 判不出来的那一类

4. 界面不变

因为有上限,原来的 live.retrying 文案(带 n/max)继续适用,
不需要新增文案,客户端零改动。

测试

  • 新增 tests/turn-retry-backoff.test.mjs(4 项):窗口形状、5xx/网络故障的识别、
    400/404/422 不重试、正常消息不误判
  • minimax-bot 全量 121 项 / 114 通过 / 7 失败 —— 与上游 main 完全相同
    (上游自己也有这 7 个失败,与本改动无关)
  • npm run validate 对本插件 OK、repository: OK;npm test 69/69

取舍

  • 没有做「暂时性错误无限重试」:那会让失败静默持续下去,也不符合
    「连续重试要设上限」的要求。现在的做法是把窗口拉长到能覆盖一次真实抖动,
    同时保留上限。
  • 没有做「400/404 也重试」:与 Host 的设计意图相反。
  • 判定仍然优先用 Host 的 retryable 标志与 reason;文本匹配只是补位,
    不是另起一套分类。

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

## 要解决的问题:一次 30 秒的服务端抖动就让 Bot 死掉

上游 0.7.4 的重试窗口是 `TURN_RETRY_DELAYS_MS = [8000, 20000]` ——
**只重试 2 次**(等 8 秒、等 20 秒),然后就放弃。

而 Host 的 `toLLMRetryDecision()` 对**通用 5xx 故意 fail-closed**:
`LLM_UPSTREAM_ERROR`(50113)是「400 及以上的兜底桶」,
把 HTTP 400/404 这种重试无意义的错误和 HTTP 500 这种**暂时性**错误混在一起,
于是分类器只能整体判成不可重试。

两条合起来的后果:
一次持续 30 秒的服务端抖动(`Internal server error (500)` / `502 Bad Gateway` /
`net::ERR_NETWORK_CHANGED` / 限流 / 模型过载)就会让回合**第一次失败就死掉**,
只能靠人手点一下才回来。Bot 团队的可用性因此被一次瞬时故障决定。

## 复现场景

1. 让 MiniMax Bot 团队里任何一个 bot 收到一条任务;
2. 在它请求模型时让上游返回一次 500(或断网、限流、网关 502);
3. 观察:该回合**只尝试了 3 次**(首次 + 2 次重试,共约 28 秒)就返回 error,
   bot 停在原地,需要人工再点一次。

## 改动

### 1. 重试窗口从 2 档延长到 10 档(**仍然有上限**)

```
8s → 20s → 40s → 60s × 7
```

总窗口约 **8 分钟**,足以自愈绝大多数瞬时抖动。

- **有上限**:第 10 次之后把原始错误原样暴露给用户,不会悄悄重试到天荒地老。
- **窗口数组仍是唯一的旋钮**:注入 `retryDelaysMs: []` 依然表示「完全不重试」,
  既有语义与既有测试一字未改(`engine-reliability.test.mjs` 51/51 通过)。

### 2. 400 / 404 / 422 **不重试**

重试它们只是把同一次拒绝重复十遍。Host 自己的注释说得很清楚:
*would turn an HTTP 400 or 404 into ten identical attempts*。

### 3. 错误文本匹配收窄

只认**明确的服务端 / 网络故障形态**:

```
net::ERR_NETWORK_CHANGED / ERR_NETWORK / ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE
socket hang up / internal server error / bad gateway|gateway timeout|service unavailable
请求处理超时 / unknown error, 520 / overloaded|rate limit|too many requests|model is busy
HTTP 500 / status 502 / code=503(要带上下文,不认裸数字)
```

- **不认裸数字** ⇒「用了 400 tokens」「第 404 步」这类正常消息不会被误判成错误
- **不含 400/404/422** ⇒ 上面那条不重试的规则
- 判定「这次失败值不值得重试」的**第一道**依据始终是 Host 自己给的
  `result.error.retryable` 标志;文本匹配只是补上 Host 判不出来的那一类

### 4. 界面不变

因为**有上限**,原来的 `live.retrying` 文案(带 `n/max`)继续适用,
不需要新增文案,客户端零改动。

## 测试

- 新增 `tests/turn-retry-backoff.test.mjs`(4 项):窗口形状、5xx/网络故障的识别、
  400/404/422 不重试、正常消息不误判
- minimax-bot 全量 **121 项 / 114 通过 / 7 失败** —— 与上游 main **完全相同**
  (上游自己也有这 7 个失败,与本改动无关)
- `npm run validate` 对本插件 **OK**、`repository: OK`;`npm test` 69/69

## 取舍

- **没有**做「暂时性错误无限重试」:那会让失败静默持续下去,也不符合
  「连续重试要设上限」的要求。现在的做法是**把窗口拉长到能覆盖一次真实抖动**,
  同时**保留上限**。
- **没有**做「400/404 也重试」:与 Host 的设计意图相反。
- 判定仍然**优先**用 Host 的 `retryable` 标志与 `reason`;文本匹配只是补位,
  不是另起一套分类。
Copilot AI balanced review requested due to automatic review settings October 20, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES(head 7e672a3)

方向对:把回合重试从 2 档拉长到 10 档(约 8 分钟、仍有上限),并用收窄的文本匹配补 Host 对通用 5xx 的 fail-closed,同时明确不把 400/404/422 当成暂时性错误。范围也清楚,只动官方 minimax-bot 的重试逻辑。

合并前请先修这一处(阻塞):

  • miniapp/node/engine.mjs:message.live 的 max 用了 this.retryMaxAttempts + 1,但构造函数里从未赋值 retryMaxAttempts(只有一句过期的「无限重试」注释)。重试时界面会变成 {n}/NaN,和 PR 里「live.retrying 带 n/max、客户端零改动」的说法不符。上限实际已经由 attempt < this.retryDelays.length 管住了,这里应改成 this.retryDelays.length + 1(或真正定义并维护 retryMaxAttempts),并删掉/改掉仍写「无限重试」「TURN_RETRY_MAX_ATTEMPTS」的过期注释。

建议一并处理(不阻塞策略门禁,但会影响发版):

  • package.json 仍是 0.7.4,.minimax-plugin/plugin.json 已是 0.7.5;mcp.mjs 的 serverInfo.version 还是 0.7.0。请与 #48 协调版本号(两边都在抢 0.7.5),后合的一方 rebase 后升到下一个版本。

已核对

  • 400/404/422 不重试(新匹配路径):isTransientErrorMessage 不含这些状态;新增测试覆盖「HTTP 400/404」「code=422」以及「用了 400 tokens」类误判。既有 canRetry 仍尊重 Host 的 retryable === false 与 NO_RETRY_REASONS。
  • 有上限:attempt < this.retryDelays.length,默认 10 档;注入 retryDelaysMs: [] 仍表示不重试。
  • 安全:diff 不涉及凭据、宿主数据库、其它插件数据;无 allowOnce / 模型自动同意。
  • 与 #48:同插件、不同关注点(重试 vs 气泡/操作栏/任务条),可以保持拆分。硬冲突主要在 README.md(两边都改);plugin.json 两边都是 0.7.4→0.7.5(内容相同,合并时仍可能撞版本语义)。engine.mjs 仅本 PR 改,无代码冲突。后合的人 rebase 并协调版本即可。

测试(本机,Asia/Shanghai)

  • npm run check:plugins/minimax-ai/minimax-bot: OK,repository: OK
  • 新增 tests/turn-retry-backoff.test.mjs:4/4 通过
  • node --test tests/*.mjs(先在 tests/ 下 npm ci 装 jsdom):204/204 通过(相对 main 的 200 条多出本 PR 的 4 条)
  • 仓库根 node --test test/*.test.mjs:69/69 通过
  • 未装 jsdom 时会有 7 个 UI 相关文件直接加载失败,与 main 相同,与本改动无关

Standards(摘要)

  • 符合 AGENTS 门禁:npm run check 通过;无宿主/用户数据破坏面。
  • 版本三处不一致(plugin.json / package.json / mcp serverInfo)违反「manifest 一致」惯例。
  • retryMaxAttempts / TURN_RETRY_MAX_ATTEMPTS 属于 Mysterious Name / 半截重构残留。

Spec(摘要)

  • PR 正文要求的窗口形状、上限、400/404/422 不进暂时性匹配、窄匹配:已实现并有单测。
  • 「界面不变、n/max 继续适用」因 retryMaxAttempts 未定义而未兑现 → 对应上面的阻塞项。
  • 注释仍残留「暂时性 ⇒ 无限重试」叙事,与最终有上限的实现不一致(清理即可)。

s: Math.round(delay / 1000),
n: attempt + 1,
// 有次数上限 ⇒ 如实告诉用户还剩几次机会。
max: this.retryMaxAttempts + 1,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

阻塞:this.retryMaxAttempts 从未赋值(构造函数里只有过期注释),这里会得到 NaN,live.retrying 的 {n}/{max} 会显示成 2/NaN。上限已经由上面的 attempt < this.retryDelays.length 保证,请改成 this.retryDelays.length + 1,或真正定义并维护 retryMaxAttempts,并清掉仍写「无限重试」/TURN_RETRY_MAX_ATTEMPTS 的注释。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修,谢谢指出。

原因是我收敛重试逻辑时,删掉了 this.retryMaxAttempts 的赋值却漏改了用法,于是 undefined + 1 = NaN。

按你的建议改成 this.retryDelays.length + 1(与上游原本的写法一致),现在 live.retrying 显示的是 n/11(10 次重试 + 首次)。

另外两条也一并处理了:

  • 过期注释:还写着「无限重试」的地方已改掉 —— 现在统一口径是「暂时性错误同样受次数上限约束」。
  • 版本号:plugin.json / package.json / mcp.mjs 的 serverInfo.version 三处之前是 0.7.5 / 0.7.4 / 0.7.0,现已统一为 0.7.5(与 fix(minimax-bot): fold the earlier bubbles of a long reply, bottom-left reply actions #48 撞号这点我在 PR 描述里也标了一句,最终顺序以维护者为准)。

你那边跑到的 204 个测试,我这边重跑也是 121 项 / 114 通过 / 7 失败 —— 那 7 个与上游 main 完全相同(上游自己也有),与本 PR 无关。

维护者 review 指出的问题:

1. **`max` 显示成 `2/NaN`**(真 bug,我引入的):
   删掉 `this.retryMaxAttempts` 的赋值时漏改了**用法**,
   `this.retryMaxAttempts + 1` ⇒ `undefined + 1` = NaN。
   ⇒ 改用 `this.retryDelays.length + 1`(与上游原本的写法一致)。

2. **清掉还写着「无限重试」的旧注释** —— 现在统一口径:
   暂时性错误**同样受次数上限约束**(第 10 次之后把原始错误暴露给用户)。

3. **版本号三处对齐 0.7.5**:`plugin.json` / `package.json` / `mcp.mjs` 的
   `serverInfo.version` 之前分别是 0.7.5 / 0.7.4 / 0.7.0。
   (与 MiniMax-AI#48 的 0.7.5 撞号是预期的,两边都需要维护者定顺序。)

测试:121 项 / 114 通过 / 7 失败 —— 与上游 main 完全相同(那 7 个是上游自己的失败)。
@Hylouis233

Copy link
Copy Markdown
Author

按你的意见改了

  • max 显示成 2/NaN(真 bug,我引入的):收敛重试逻辑时删掉了 this.retryMaxAttempts 的赋值却漏改了用法,undefined + 1 = NaN。⇒ 改用 this.retryDelays.length + 1(与上游原本写法一致),现在显示 n/11。
  • 过期注释:还写着「无限重试」的地方都改了 —— 现在统一口径是「暂时性错误同样受次数上限约束」。
  • 版本号三处对齐 0.7.5:plugin.json / package.json / mcp.mjs 的 serverInfo.version 之前是 0.7.5 / 0.7.4 / 0.7.0。

关于版本号撞 #48:我这边就先用 0.7.5,哪个 PR 先合、另一个就 rebase 改成 0.7.6,以维护者为准。

测试:121 项 / 114 通过 / 7 失败 —— 与上游 main 完全相同(那 7 个上游自己也有,与本 PR 无关)。你那边跑到的 204 个也全过。

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢更新!上一轮的阻塞问题已经修好了,复核结果如下:

已解决

  • max: this.retryDelays.length + 1 修复正确:this.retryMaxAttempts 已无任何引用;attempt 计的是重试次数,n = attempt + 1 在默认配置下从 2 走到 11,与总尝试次数(首次 1 次 + 重试 10 次)= 11 一致,不再出现 n/NaN,也没有差一错误。注入 retryDelaysMs: [] 时根本不会进入重试分支,不会显示奇怪的 x/1。
  • 「无限重试」的过期注释已清理。
  • 三处版本号已对齐。

需要修改(阻塞)

  • main 已经通过 #48 发布了 minimax-bot 0.7.5(main 当前为 0c187f3)。本 PR 基于更早的 9804030,合入后三处版本号会和 main 完全相同,等于这次重试改动没有新版本号、用户侧收不到更新。另外 #50 也计划用 0.7.6;请 rebase 到最新 main,并在 rebase 时 main 上的版本号基础上再升一号(如果 #50 先合就是 0.7.7),三处(package.json、.minimax-plugin/plugin.json、mcp.mjs 的 serverInfo.version)保持一致。
    • 我本地试合并过:没有实际冲突(README.md 自动合并成功),合并后 npm run check 通过,插件测试 211/211 通过。所以 rebase 应该很顺利。

非阻塞小建议(可顺手改)

  • engine.mjs 中还有一处注释引用了不存在的 TURN_RETRY_MAX_ATTEMPTS。
  • 总窗口时长说法不一致:文件头和测试标题写「约 8 分钟」,第 652 行注释和测试文件头注释写「约 7.5 分钟」、「60s×6」;实际是 8+20+40+60×7 = 488 秒 ≈ 8 分钟,60s 档是 7 个。
  • this.retryDelays 下面那行 /** 重试窗口的退避基数与上限… */ 现在没有对应字段,成了悬空注释,建议删掉或合进上一行注释。

测试(Node 22 npm run check;Node 20 node --test tests)

  • PR head:check 通过;插件测试 204/204 通过
  • main 基线:check 通过;207/207 通过(数量差异来自 #48 新增的测试,PR 尚未包含)
  • PR 合入 main 后:check 通过;211/211 通过

{
"name": "minimax-bot",
"version": "0.7.4",
"version": "0.7.5",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请 rebase 后在 main 当时的版本号上再升一号,与另外两处保持一致。

"name": "minimax-bot",
"displayName": "MiniMax Bot",
"version": "0.7.4",
"version": "0.7.5",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请 rebase 后在 main 当时的版本号上再升一号,与另外两处保持一致。

protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0],
capabilities: { tools: { listChanged: false } },
serverInfo: { name: 'minimax-bot', version: '0.7.0' },
serverInfo: { name: 'minimax-bot', version: '0.7.5' },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请 rebase 后在 main 当时的版本号上再升一号,与另外两处保持一致。

// 抛异常 ⇒ rawFailureMessage 有内容 ⇒ transient 为真 ⇒ 可重试。
// ⚠️ 「暂时性」**不再**用来绕过下面两道判据:
// - 不再绕过 `NO_RETRY_REASONS`(auth / quota / invalid_request 等)
// - 不再绕过次数上限(见 `TURN_RETRY_MAX_ATTEMPTS`)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(非阻塞)这里引用的 TURN_RETRY_MAX_ATTEMPTS 并不存在,上限现在就是 this.retryDelays.length,建议改一下注释。

!input.answer &&
!toolStarted &&
// ⚠️ **有上限**(2026-10-20 收敛):暂时性错误同样受次数上限约束。
// 次数上限就是 `this.retryDelays.length`(默认 10 档 ⇒ 约 7.5 分钟窗口),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(非阻塞)「约 7.5 分钟」与文件头的「约 8 分钟」不一致;默认窗口合计 488 秒,建议统一为「约 8 分钟」(测试文件头注释里的「60s×6,约 7.5 分钟」同理,实际是 60s×7)。

@July-X

July-X commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

提示一下合并前需要处理的两件事(这个 PR 现在和 main 冲突,麻烦 rebase 一下):

1. 冲突在 4 个文件:plugin.json、package.json、mcp.mjs(版本号三处)和 README.md。功能代码本身(engine.mjs 和新增的 tests/turn-retry-backoff.test.mjs)合得上,不用动。

2. 版本号要重新对:这个分支写的是 0.7.5,但 main 现在已经是 0.7.6,直接合进去会把版本号往回退。建议等 #52 合并后 rebase 到最新 main,再把版本三处按当时的下一个版本号写(也就是 0.7.8)。如果现在就 rebase,版本要先写 0.7.7,等 #52 合进来还得再升一次。

rebase 之后麻烦再跑一遍 npm run check 和包内测试,README 的冲突段落在 Background work 那一段。

@Hylouis233

Copy link
Copy Markdown
Author

已按你说的处理(#52 合并后再同步 main):

  • 把最新 main(c3850b0)合进本分支,冲突只在版本号三处:plugin.json、package.json、mcp.mjs 统一改为 0.7.8(main 当前是 0.7.7)。
  • README 的 Background work 段落自动合并,没有改动 main 的内容;本 PR 新增的说明小节标题和正文里的版本号同步改为 0.7.8。
  • engine.mjs 与 tests/turn-retry-backoff.test.mjs 没有改动。

本地验证(Node v22.11.0,head 97511cb):

  • npm run check:通过,plugins/minimax-ai/minimax-bot: OK,仓库测试 69/69 通过。
  • 包内测试 node --test tests/*.test.mjs:209/209 通过。

现在 PR 已是可合并状态,麻烦再看一下。

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢更新!复审 97511cb。npm run validate 通过;新增的 turn-retry-backoff 测试 4/4 通过,engine-reliability 全部通过(其余 minimax-bot UI 测试的失败在 main 上同样存在,与本 PR 无关)。已开始调用工具的回合不会被重试(!toolStarted 保留了),这一点很好。

但这次的改法我认为偏重了,有一个需要先修的问题,几处设计上希望收敛:

必须修改

  1. 抛异常路径会无视 retryable: false(engine.mjs:641-657)。 异常路径里 result 是 undefined,所以 657 行的 result?.error?.retryable !== false 和 NO_RETRY_REASONS 都不起作用;而 641 行的 failed || transient 让「消息里碰巧含有 ETIMEDOUT / rate limit」的异常绕过了异常自己声明的 retryable: false。我本地用一个假的 agent 复现:抛出 { code: 'AGENT_OUTCOME_UNKNOWN', retryable: false, message: '... ETIMEDOUT' },main 上只执行 1 次,本 PR 执行了 4 次(retryDelaysMs: [1,1,1])。AGENT_OUTCOME_UNKNOWN 表示这一轮可能已经执行过了,自动重发可能导致重复回复/重复操作——代码其他地方(727 行)也明确不对它提供重试。另外抛出 retryable: false, message: 'rate limit: Token Plan quota' 同样被重试了 4 次。请让显式的 retryable === false 一票否决(异常和 result 两条路径都是),AGENT_OUTCOME_UNKNOWN 永不自动重试,并补一条引擎级测试。

建议修改(设计)

  1. 默认重试从 2 次(约 28 秒)提到 10 次(约 7.5 分钟)作用于所有可重试错误,不只是新增的「暂时性」那一类。这是一个比较大的行为变化:一个失败回合会占着该 Bot 的运行槽、显示「重试中」最长近 8 分钟,每次重试(已到达会话的情况下)还会往会话里追加一条「继续」提示。建议默认保持 [8000, 20000];如果确实需要更长的窗口,单独做成一个可配置项,默认关闭或窗口更短。README 第 99 行也还写着「8 s 和 20 s 后重试」,与现在不一致。
  2. 用关键字匹配错误消息来推翻「不可重试」的判断比较脆弱(engine.mjs:68-83)。 internal server error、rate limit、ERR_NETWORK 这类字符串可能出现在任何错误文本里(例如限额类错误也常带 "rate limit"),而分类结果直接决定是否自动重发用户的话。另外在 result 路径上 failed 已经等于 status === 'error',transient 对它其实不增加任何东西——整套正则实际只影响抛异常的路径,也就是上面第 1 点出问题的地方。建议优先依赖结构化的 retryable / reason / code,去掉这层文本匹配;如果一定要保留,范围收窄并只在没有显式 retryable 时生效。
  3. 请删除注释和 README 中对宿主实现细节的引用(engine.mjs:55-62、615-618,README 0.7.8 小节第 39 行附近)。插件注释只描述插件自己的行为即可,不要依赖或引用宿主内部实现。

小问题

  1. engine.mjs:190 残留一行没有对应字段的注释,请删除。
  2. 注释与实现不一致:43 行「约 8 分钟」vs 652 行「约 7.5 分钟」;619 行「一直重试到成功」、621 行「2 次 / 28 秒」、639 行引用了不存在的 TURN_RETRY_MAX_ATTEMPTS;33 行旧的英文注释与新注释重复。642-646 行 reason !== 'auth' && reason !== 'quota' 与 NO_RETRY_REASONS 重复。
  3. rawFailureMessage 没有在循环开头和其他字段一起重置,容易在后续修改中带入上一轮的错误文本。
  4. tests/turn-retry-backoff.test.mjs:17 复制了一份默认数组再断言它自己,不能保护 engine.mjs 里的真实值;建议导出常量或通过引擎行为测试。目前也没有覆盖重试循环本身(上面第 1 点)的测试。
  5. 版本提醒:#56 也在升 0.7.8,后合并的一方需要升到 0.7.9。

更简单的方案供参考:保持现有 2 次重试不变,只修正「宿主未给出 retryable 时」的默认判断(或加一个可选的更长窗口配置),无需正则和十档延迟。

// - 不再绕过 `NO_RETRY_REASONS`(auth / quota / invalid_request 等)
// - 不再绕过次数上限(见 `TURN_RETRY_MAX_ATTEMPTS`)
// 它只用来决定**退避用多长**。
const retryableFailure =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

需修改:抛异常时 result 为 undefined,657 行的 retryable !== false 和 NO_RETRY_REASONS 都不生效,而 failed || transient 会让带 retryable: false 的异常仅因消息含 ETIMEDOUT / rate limit 就被重试。本地复现:AGENT_OUTCOME_UNKNOWN + retryable:false + 'ETIMEDOUT',main 执行 1 次,本 PR 执行 4 次——这一轮可能已经执行过,重发会产生重复。请让显式 retryable === false 一票否决,AGENT_OUTCOME_UNKNOWN 不自动重试,并补引擎级测试。

* ⚠️ **有上限**:第 10 次之后就把原始错误暴露给用户,不让它一直转下去。
* ⚠️ **这个数组就是唯一的旋钮**:注入空数组 = 完全不重试(既有语义不变)。
*/
const TURN_RETRY_DELAYS_MS = [8000, 20000, 40000, 60000, 60000, 60000, 60000, 60000, 60000, 60000];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

默认从 2 次(~28s)提到 10 次(~7.5min)会作用于所有可重试错误,不只暂时性错误:失败回合会占用 Bot 运行槽近 8 分钟,并可能多次追加「继续」提示。建议默认保持 [8000, 20000],更长窗口做成单独配置项(默认关闭或更短)。

/open platform service unavailable/i,
/请求处理超时/,
/unknown error, *520/,
/overloaded|rate limit|too many requests|model is busy/i,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

按关键字匹配错误文本来决定是否自动重发比较脆弱,例如限额类错误也常含 "rate limit"。而且 result 路径上 failed 已等于 status === 'error',这套正则实际只影响抛异常路径。建议依赖结构化的 retryable/reason/code,去掉或大幅收窄这层匹配。

/**
* 暂时性错误判定:**只匹配明确的服务端 / 网络故障形态**。
*
* 为什么需要它:Host 的 `toLLMRetryDecision()` 对**通用 5xx** 故意 fail-closed ——

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请删除这里(以及 615-618 行、README 0.7.8 小节)对宿主实现细节的引用,注释只描述插件自身行为即可。

this.logger = deps.logger;
/** Waits before each automatic retry of a failed turn (tests shorten them). */
this.retryDelays = deps.retryDelaysMs ?? TURN_RETRY_DELAYS_MS;
/** 重试窗口的退避基数与上限(测试会把它调小)。 */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

残留注释,下面没有对应字段,请删除。

// 但本插件看得到错误消息文本,能把「服务端抖了一下」与
// 「服务端明确拒绝」分开。实测这批 50113 全是暂时性故障
// (网络切换 / 500 / 超时 / 520 / 502 / 服务不可用)。
// ⇒ 一直重试到成功,退避封顶 60 秒。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释与实现不符:这里「一直重试到成功」、621 行「2 次 / 28 秒」、639 行引用不存在的 TURN_RETRY_MAX_ATTEMPTS、43 行 8 分钟 vs 652 行 7.5 分钟。另外 644-645 行与 NO_RETRY_REASONS 重复。

import { isTransientErrorMessage } from '../miniapp/node/engine.mjs';

/** 与 engine.mjs 里的默认窗口保持一致(改一处就要同步这里)。 */
const DEFAULT_WINDOW_MS = [8000, 20000, 40000, 60000, 60000, 60000, 60000, 60000, 60000, 60000];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里复制了一份数组再断言它自己,保护不了 engine.mjs 的真实默认值;建议导出常量或改成引擎行为测试,并覆盖 retryable:false 异常不被重试的场景。

Hylouis233 and others added 3 commits October 22, 2026 18:38
…s, keep the default window

- An explicit `retryable: false` now ends the turn on both the thrown and the
  returned path, and `AGENT_OUTCOME_UNKNOWN` is never retried automatically.
- Drop the error-text matching; the retry decision uses only the structured
  `retryable`, `reason` and `code`.
- Keep the default window at two retries (8 s, 20 s). The ten-step window
  (about 8 minutes) is now opt-in through the `longRetry` setting.
- Remove references to Host internals from comments and the README, and tidy
  the retry comments.
- Test the real exported windows and the engine's retry loop, including the
  thrown `AGENT_OUTCOME_UNKNOWN` + `retryable: false` case.
…efault retries

Follow the simplified plan from the 2026-10-22 review: no `longRetry` setting
and no 10-step window. The default stays at two retries (8 s, 20 s).

- `mayRetryFailure` holds the retry rule in one place: an explicit
  `retryable: false` always vetoes, on the thrown and the returned path;
  `AGENT_OUTCOME_UNKNOWN` is never resent; `NO_RETRY_REASONS` now also applies
  to a thrown error.
- Export `TURN_RETRY_DELAYS_MS`, `NO_RETRY_REASONS` and `mayRetryFailure` and
  assert the real values; the engine-level loop tests stay in
  turn-retry-veto.test.mjs.
- README 0.7.8 section describes only the plugin's own behaviour.
@Hylouis233

Copy link
Copy Markdown
Author

按 10-22 review 里「更简单的方案」重新收敛了,当前 head 是 71848a3。上一条评论里写的可选 longRetry 配置在这次提交里已经删掉,以本条为准。

必须修改

  1. retryable: false / AGENT_OUTCOME_UNKNOWN:判定集中到 mayRetryFailure()。显式 retryable === false 一票否决,抛异常和返回 result 两条路径都一样。AGENT_OUTCOME_UNKNOWN(抛出的 code 或 result.error.code)永不自动重试,即使它带 retryable: true 也不重试。抛异常路径现在也看 NO_RETRY_REASONS。引擎级测试 tests/turn-retry-veto.test.mjs 用假 agent 和 retryDelaysMs: [1,1,1] 跑真实的 runBotTurn,覆盖了你复现的两个用例(AGENT_OUTCOME_UNKNOWN + retryable:false + ETIMEDOUT,retryable:false + "rate limit: Token Plan quota"),现在都只执行 1 次。

设计
2. 默认保持 2 次重试 [8000, 20000]。十档窗口删掉了,也不加可选的长窗口配置(server.mjs / store.mjs 恢复成和 main 一样)。
3. TRANSIENT_ERROR_PATTERNS / isTransientErrorMessage 这套错误文本匹配删掉了,只看结构化的 retryable / reason / code。
4. 注释和 README 里引用宿主实现的内容都删掉了。README 0.7.8 小节只写插件自身的行为,Reliability 段里「after 8 s and 20 s」和实现一致。

小问题
5–7. 悬空注释、「约 8 分钟 / 7.5 分钟」「一直重试到成功」、TURN_RETRY_MAX_ATTEMPTS、重复的 auth/quota 判断、重复的头注释都随重写去掉了。rawFailureMessage 只给文本匹配用,也一起删了。
8. 现在导出 TURN_RETRY_DELAYS_MS(已 freeze)、NO_RETRY_REASONS、mayRetryFailure。tests/turn-retry-backoff.test.mjs 断言真实常量,并检查引擎默认用的就是它。
9. 版本:main 现在是 0.7.7,#56 还没合,所以本 PR 三处(package.json、plugin.json、mcp.mjs serverInfo)和 README 小节标题统一为 0.7.8。#56 也在升 0.7.8,后合的一方需要改成 0.7.9。

测试(Node v22.11.0,head 71848a3,已包含 main 9269f35)

  • npm run check:plugins/minimax-ai/minimax-bot: OK,仓库测试 69/69 通过
  • 插件目录 node --test tests/*.test.mjs(按 README 先 npm ci --prefix tests):216/216 通过,0 失败
  • 直接跑 node --test tests:Node 22.11 会把 tests 当成模块路径,报 Cannot find module .../tests,所以用了 README 里的 glob 写法

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants