From 7e672a3dc26301ec8684121375acafeacce71f0c Mon Sep 17 00:00:00 2001 From: Hylouis233 Date: Tue, 20 Oct 2026 22:04:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(minimax-bot):=20=E5=9B=9E=E5=90=88?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E4=BB=8E=202=20=E6=AC=A1=E5=BB=B6=E9=95=BF?= =?UTF-8?q?=E5=88=B0=2010=20=E6=A1=A3=EF=BC=88=E4=BB=8D=E6=9C=89=E4=B8=8A?= =?UTF-8?q?=E9=99=90=EF=BC=89=EF=BC=8C400/404/422=20=E4=B8=8D=E9=87=8D?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 要解决的问题:一次 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`;文本匹配只是补位, 不是另起一套分类。 --- .../minimax-bot/.minimax-plugin/plugin.json | 2 +- plugins/minimax-ai/minimax-bot/README.md | 31 +++++ .../minimax-bot/miniapp/node/engine.mjs | 119 +++++++++++++++++- .../tests/turn-retry-backoff.test.mjs | 70 +++++++++++ 4 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 plugins/minimax-ai/minimax-bot/tests/turn-retry-backoff.test.mjs diff --git a/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json b/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json index 4ae17ba..ae87257 100644 --- a/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json +++ b/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "name": "minimax-bot", "displayName": "MiniMax Bot", - "version": "0.7.4", + "version": "0.7.5", "description": "Turn MiniMax Code into an always-on team of Bots. A Chief Bot helps you build the team; in group chats the admin answers and @mentions hand work to other members. Every Bot has its own octopus avatar and memory, with scheduled tasks and usage stats.", "author": "MiniMax Code", "icon": "icon.png", diff --git a/plugins/minimax-ai/minimax-bot/README.md b/plugins/minimax-ai/minimax-bot/README.md index 1b7a956..6139f59 100644 --- a/plugins/minimax-ai/minimax-bot/README.md +++ b/plugins/minimax-ai/minimax-bot/README.md @@ -28,6 +28,37 @@ Copy this directory, including the hidden `.minimax-plugin/` directory, into the - **Language**: English and Chinese. By default it follows the operating system's preferred language (the first English or Chinese entry in `navigator.languages`; other languages fall back to English), or you can fix it under Settings → General → Language. The interface, errors, system notices, Chief's default setup, the context Bots receive and the MCP tool descriptions all switch; replies the Bots wrote and text you entered stay as they are. - **Reactions**: react to finished Bot replies in private and group chats. The quick bar offers 👍 ❤️ 🙌 😂 😭 😮, and more emoji can be browsed by category or searched in English or Chinese. Each message holds one reaction: picking a new one replaces it, and picking the same one again removes it. Reactions are saved locally, survive refreshes and restarts, and sync to other open windows. They do not trigger a Bot reply and are not sent to the model. +## 0.7.5 Transient failures get a longer, still-capped retry window + +A Bot that died from a **temporary** failure now comes back on its own. + +The Host classifies a generic 5xx as `LLM_UPSTREAM_ERROR` (50113) and deliberately +fails closed on it: that code is a catch-all bucket for *any* status of 400 or above +except 402/401/403/429/529, so retrying it blindly could turn an HTTP 400 or 404 +into ten identical attempts. The result is that a plain HTTP 500 — which usually +clears on its own within seconds — killed the turn on the first try. + +This plugin sees the error **message**, so it can tell the two apart. Failures that +name a transient condition (a network error, a 5xx, a gateway timeout, rate limiting, +an overloaded model) are retried; failures that are a decision rather than a glitch +are not. + +What changed in 0.7.5: + +- **The retry window is longer.** 0.7.4 retried **twice** (after 8 s and 20 s), so a + blip that lasted 30 seconds killed the Bot. The window is now + `8s → 20s → 40s → 60s × 7` — ten attempts over roughly **eight minutes**, which + covers the overwhelming majority of transient outages. +- **There is still a cap.** After the last attempt the original error surfaces as + it is. The window is also still the single knob: injecting + `retryDelaysMs: []` keeps meaning "do not retry at all". +- **400 / 404 / 422 are never retried.** Retrying them only repeats the same + rejection ten times. +- **The message match is narrow.** It requires an explicit server-side or network + shape (`net::ERR_NETWORK_CHANGED`, `Internal server error (500)`, `502 Bad + Gateway`, `rate limit exceeded`, `HTTP 500`, …), so ordinary text such as + "used 400 tokens" or "step 404 done" is never mistaken for an error. + ## 0.7.0 Reliable delegation and result delivery Chat with your Bot as usual. It can ask other Bots for help while you keep chatting; completed results return here, and you can stop the work at any time. diff --git a/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs b/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs index ba35635..bbb2866 100644 --- a/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs +++ b/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs @@ -33,10 +33,67 @@ const HELPER_ACTIVE = new Set(['queued', 'running']); * A turn that failed before running any tool is retried this many more times, after these waits. * Model capacity errors (429/529) and timeouts usually clear within a minute. */ -const TURN_RETRY_DELAYS_MS = [8000, 20000]; +/** + * 重试窗口:**延迟序列 + 次数上限**(一个旋钮,两个含义 —— 与上游一致)。 + * + * ⚠️ 为什么改(2026-10-20):上游 0.7.4 只有 `[8000, 20000]` ⇒ **只重试 2 次** + * (等 8 秒、等 20 秒)⇒ 一次持续 30 秒的服务端抖动就让 bot **直接死掉**, + * 只能靠人手点一下才回来。 + * ⇒ 延长到 10 档:8s → 20s → 40s → 60s ×7(指数退避、封顶 60 秒), + * 总窗口约 **8 分钟**,足以自愈绝大多数瞬时抖动。 + * + * ⚠️ **仍然有上限**(不是无限重试):第 10 次之后就把原始错误暴露给用户。 + * ⚠️ **这个数组就是唯一的旋钮**:注入空数组 = 完全不重试(既有语义不变)。 + */ +const TURN_RETRY_DELAYS_MS = [8000, 20000, 40000, 60000, 60000, 60000, 60000, 60000, 60000, 60000]; /** Failure reasons a retry cannot fix; anything else (or no reason from an older Host) may be retried. */ const NO_RETRY_REASONS = new Set(['auth', 'quota', 'context_too_long', 'blocked', 'invalid_request']); +/** + * 暂时性错误判定:**只匹配明确的服务端 / 网络故障形态**。 + * + * 为什么需要它:Host 的 `toLLMRetryDecision()` 对**通用 5xx** 故意 fail-closed —— + * `LLM_UPSTREAM_ERROR`(50113)是「400 及以上的兜底桶」,把 HTTP 400/404 这种 + * 重试无意义的错误和 HTTP 500 这种**暂时性**错误混在一起,于是分类器只能整体 + * 判成不可重试 ⇒ 一次 30 秒的服务端抖动就会让 bot **直接死掉**。 + * + * ⚠️ **这里只认「明确是服务端/网络故障」的形态**(2026-10-20 收敛): + * - **不含 400 / 404 / 422**:Host 的注释说得很清楚 —— + * *would turn an HTTP 400 or 404 into ten identical attempts*。 + * 重试它们只是把同一次拒绝重复十遍。`invalid_request` 也在 `NO_RETRY_REASONS` 里。 + * - **不含裸数字**:否则「用了 400 tokens」「第 404 步」这类正常消息会被误判成错误。 + * 判定「这次失败值不值得重试」的**第一道**依据始终是 Host 自己给的 + * `result.error.retryable` 标志;这里只是补上 Host 判不出来的那一类。 + */ +const TRANSIENT_ERROR_PATTERNS = [ + /net::ERR_NETWORK_CHANGED/i, + /ERR_NETWORK/i, + /ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE/i, + /socket hang up/i, + /internal server error/i, + /bad gateway|gateway timeout|service unavailable/i, + /open platform service unavailable/i, + /请求处理超时/, + /unknown error, *520/, + /overloaded|rate limit|too many requests|model is busy/i, + // 5xx 只认带上下文的形态(`HTTP 500` / `status 502` / `code=503`), + // 不认裸数字 —— 「用了 500 tokens」不是错误。 + /\b(?:http|status(?:\s+code)?|code)\s*[:=]?\s*50[0234]\b/i, + /\b50[0234]\s+(bad gateway|gateway timeout|service unavailable|internal server error)/i, +]; + +/** + * 这条错误消息是不是「暂时性」的(重试有意义)。 + * + * @param {string} message + * @returns {boolean} + */ +export function isTransientErrorMessage(message) { + const text = String(message ?? ''); + if (!text) return false; + return TRANSIENT_ERROR_PATTERNS.some((re) => re.test(text)); +} + /** Resolves true after `ms`, or false at once if `signal` aborts first. */ function sleepUnlessAborted(ms, signal) { return new Promise((resolve) => { @@ -130,6 +187,7 @@ export class Engine { this.logger = deps.logger; /** Waits before each automatic retry of a failed turn (tests shorten them). */ this.retryDelays = deps.retryDelaysMs ?? TURN_RETRY_DELAYS_MS; + /** 无限重试的退避基数与上限(测试会把它调小)。 */ /** @type {{ at: number, value: Promise } | undefined} */ this.catalogCache = undefined; /** @type {Map} helper id -> run */ @@ -505,6 +563,10 @@ export class Engine { let failure = ''; let failureCode = ''; let failureRetryable = false; + // 原始错误消息(未翻译):暂时性错误的判定必须看**它**—— + // `failure` 是 `describeAgentError()` 翻译后的文案(可能是「模型不可用」这类 + // 泛化说法),里面已经没有 500 / 网络切换这些关键信息了。 + let rawFailureMessage = ''; let attempt = 0; try { for (;;) { @@ -536,31 +598,80 @@ export class Engine { failure = describeAgentError(retryError); failureCode = errorCode(retryError); failureRetryable = Boolean(/** @type {any} */ (retryError)?.retryable); + rawFailureMessage = errorText(retryError); } } else { failure = describeAgentError(error); failureCode = errorCode(error); failureRetryable = Boolean(/** @type {any} */ (error)?.retryable); + rawFailureMessage = errorText(error); } } const failed = Boolean(failure) ? failureRetryable : result?.status === 'error'; const reason = result?.error?.reason; + // ── 两条重试路径(2026-10-19 实测补)── + // + // A) **暂时性错误 ⇒ 无限重试**。 + // Host 把通用 5xx 归进 50113 这个「兜底桶」并 fail-closed, + // 但本插件看得到错误消息文本,能把「服务端抖了一下」与 + // 「服务端明确拒绝」分开。实测这批 50113 全是暂时性故障 + // (网络切换 / 500 / 超时 / 520 / 502 / 服务不可用)。 + // ⇒ 一直重试到成功,退避封顶 60 秒。 + // + // B) **其它可重试错误 ⇒ 有限重试**(原有行为,2 次 / 28 秒窗口)。 + // + // 两条路都排除:额度类(quota)、鉴权类(auth)、上下文过长、被拒、 + // 参数非法 —— 这些重试一万次也没用。 + // ⚠️ 成功后必须**立刻停止**重试。 + // `rawFailureMessage` 保留着上一次失败的内容,而 `transient` 只看它 —— + // 于是第 4 次已经 `result.status === 'finished'` 了,却因为「上一次是 + // 暂时性错误」而继续重试(实测 attempts 从 4 一路涨到 7,永远停不下来)。 + // ⇒ 判据必须先看**本次**的结果是否成功。 + const succeeded = result?.status === 'finished'; + const transient = + !succeeded && + isTransientErrorMessage(result?.error?.message ?? rawFailureMessage ?? ''); + // ⚠️ 抛异常的路径里 `result` 是 undefined,所以不能用 `result?.status`。 + // 那种情况下「暂时性」由**原始错误消息**判定: + // 抛异常 ⇒ rawFailureMessage 有内容 ⇒ transient 为真 ⇒ 可重试。 + // ⚠️ 「暂时性」**不再**用来绕过下面两道判据: + // - 不再绕过 `NO_RETRY_REASONS`(auth / quota / invalid_request 等) + // - 不再绕过次数上限(见 `TURN_RETRY_MAX_ATTEMPTS`) + // 它只用来决定**退避用多长**。 + const retryableFailure = + failed || + (transient && + !NO_RETRY_REASONS.has(reason) && + reason !== 'auth' && + reason !== 'quota'); const canRetry = - failed && + retryableFailure && !input.answer && !toolStarted && + // ⚠️ **有上限**(2026-10-20 收敛):暂时性错误**不再**无限重试。 + // 次数上限就是 `this.retryDelays.length`(默认 10 档 ⇒ 约 7.5 分钟窗口), + // 注入空数组仍然表示「完全不重试」(既有语义一字未改)。 attempt < this.retryDelays.length && !controller.signal.aborted && !this.disposed && result?.error?.retryable !== false && !NO_RETRY_REASONS.has(reason); if (!canRetry) break; - const delay = this.retryDelays[attempt]; + // 延迟 = 窗口里的那一档(可注入 ⇒ 既有语义与既有测试都不受影响)。 + const delay = this.retryDelays[attempt] ?? 0; attempt += 1; // Words the failed attempt streamed are not the reply; the retry writes its own. message.parts = message.parts.filter((part) => part.type !== 'text'); liveText = undefined; - message.live = { key: 'live.retrying', params: { s: Math.round(delay / 1000), n: attempt + 1, max: this.retryDelays.length + 1 } }; + message.live = { + key: 'live.retrying', + params: { + s: Math.round(delay / 1000), + n: attempt + 1, + // 有次数上限 ⇒ 如实告诉用户还剩几次机会。 + max: this.retryMaxAttempts + 1, + }, + }; flush(true); const waited = await sleepUnlessAborted(delay, controller.signal); message.live = undefined; diff --git a/plugins/minimax-ai/minimax-bot/tests/turn-retry-backoff.test.mjs b/plugins/minimax-ai/minimax-bot/tests/turn-retry-backoff.test.mjs new file mode 100644 index 0000000..6959201 --- /dev/null +++ b/plugins/minimax-ai/minimax-bot/tests/turn-retry-backoff.test.mjs @@ -0,0 +1,70 @@ +// 回合级重试:重试窗口 + 暂时性错误判定。 +// +// 2026-10-20 收敛(依据上游 review 意见): +// - 上游 0.7.4 的窗口是 `[8000, 20000]` ⇒ **只重试 2 次** +// ⇒ 一次持续 30 秒的服务端抖动就让 bot **直接死掉**,只能靠人手点一下。 +// - 但「无限重试」又与维护者「给连续重试设个上限」的要求相反。 +// ⇒ 收敛成:**默认窗口延长到 10 档(8s→20s→40s→60s×6,约 7.5 分钟)**,仍然有上限。 +// +// ⚠️ 同时守住两条边界: +// 1. 400 / 404 / 422 **不重试**(重试它们只是把同一次拒绝重复十遍) +// 2. 正常消息(「用了 400 tokens」「第 404 步」)不会被误判成错误 +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { isTransientErrorMessage } from '../miniapp/node/engine.mjs'; + +/** 与 engine.mjs 里的默认窗口保持一致(改一处就要同步这里)。 */ +const DEFAULT_WINDOW_MS = [8000, 20000, 40000, 60000, 60000, 60000, 60000, 60000, 60000, 60000]; + +test('默认重试窗口:10 档、指数退避、封顶 60 秒,总窗口约 8 分钟', () => { + assert.equal(DEFAULT_WINDOW_MS.length, 10, '必须有明确的上限(不是无限重试)'); + assert.equal(DEFAULT_WINDOW_MS[0], 8000); + assert.equal(DEFAULT_WINDOW_MS[1], 20000); + assert.equal(DEFAULT_WINDOW_MS[2], 40000); + for (const d of DEFAULT_WINDOW_MS.slice(3)) assert.equal(d, 60000, '之后一律封顶 60s'); + const total = DEFAULT_WINDOW_MS.reduce((a, b) => a + b, 0); + assert.ok(total >= 7 * 60_000 && total <= 9 * 60_000, `总窗口应约 8 分钟,实际 ${total}ms`); +}); + +test('暂时性错误会被认出来(服务端 / 网络故障)', () => { + for (const m of [ + 'net::ERR_NETWORK_CHANGED', + 'Internal server error (500)', + '502 Bad Gateway', + 'service unavailable', + '请求处理超时 (2066)', + 'unknown error, 520 (1000)', + 'rate limit exceeded', + 'The model is busy right now', + 'HTTP 500', + 'status 502', + ]) { + assert.equal(isTransientErrorMessage(m), true, `应判为暂时性:${m}`); + } +}); + +test('400 / 404 / 422 不重试(重试只是把同一次拒绝重复十遍)', () => { + for (const m of [ + 'HTTP 400 Bad Request', + 'HTTP 404 Not Found', + 'code=422', + '400 Bad Request', + '404 Not Found', + ]) { + assert.equal(isTransientErrorMessage(m), false, `不该重试:${m}`); + } +}); + +test('正常消息不会被误判成错误', () => { + for (const m of [ + '用了 400 tokens', + '第 404 步完成', + '剩余 422 积分', + '当前已达到 Token Plan 用量上限 (2067)', + '积分已耗尽', + '', + undefined, + ]) { + assert.equal(isTransientErrorMessage(m), false, `不该判为暂时性:${m}`); + } +}); From 9a42b99cb15bdd01452ae7fc23c2e0dafe9f6287 Mon Sep 17 00:00:00 2001 From: Hylouis233 Date: Wed, 21 Oct 2026 11:49:38 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(minimax-bot):=20=E4=BF=AE=20max=20?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=88=90=202/NaN=E3=80=81=E6=B8=85=E6=8E=89?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E7=9A=84=E3=80=8C=E6=97=A0=E9=99=90=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E3=80=8D=E6=B3=A8=E9=87=8A=E3=80=81=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E4=B8=89=E5=A4=84=E5=AF=B9=E9=BD=90=200.7.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 维护者 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。 (与 #48 的 0.7.5 撞号是预期的,两边都需要维护者定顺序。) 测试:121 项 / 114 通过 / 7 失败 —— 与上游 main 完全相同(那 7 个是上游自己的失败)。 --- plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs | 10 +++++----- plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs | 2 +- plugins/minimax-ai/minimax-bot/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs b/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs index bbb2866..678da49 100644 --- a/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs +++ b/plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs @@ -42,7 +42,7 @@ const HELPER_ACTIVE = new Set(['queued', 'running']); * ⇒ 延长到 10 档:8s → 20s → 40s → 60s ×7(指数退避、封顶 60 秒), * 总窗口约 **8 分钟**,足以自愈绝大多数瞬时抖动。 * - * ⚠️ **仍然有上限**(不是无限重试):第 10 次之后就把原始错误暴露给用户。 + * ⚠️ **有上限**:第 10 次之后就把原始错误暴露给用户,不让它一直转下去。 * ⚠️ **这个数组就是唯一的旋钮**:注入空数组 = 完全不重试(既有语义不变)。 */ const TURN_RETRY_DELAYS_MS = [8000, 20000, 40000, 60000, 60000, 60000, 60000, 60000, 60000, 60000]; @@ -187,7 +187,7 @@ export class Engine { this.logger = deps.logger; /** Waits before each automatic retry of a failed turn (tests shorten them). */ this.retryDelays = deps.retryDelaysMs ?? TURN_RETRY_DELAYS_MS; - /** 无限重试的退避基数与上限(测试会把它调小)。 */ + /** 重试窗口的退避基数与上限(测试会把它调小)。 */ /** @type {{ at: number, value: Promise } | undefined} */ this.catalogCache = undefined; /** @type {Map} helper id -> run */ @@ -611,7 +611,7 @@ export class Engine { const reason = result?.error?.reason; // ── 两条重试路径(2026-10-19 实测补)── // - // A) **暂时性错误 ⇒ 无限重试**。 + // A) **暂时性错误 ⇒ 值得重试**(但仍然受次数上限约束)。 // Host 把通用 5xx 归进 50113 这个「兜底桶」并 fail-closed, // 但本插件看得到错误消息文本,能把「服务端抖了一下」与 // 「服务端明确拒绝」分开。实测这批 50113 全是暂时性故障 @@ -648,7 +648,7 @@ export class Engine { retryableFailure && !input.answer && !toolStarted && - // ⚠️ **有上限**(2026-10-20 收敛):暂时性错误**不再**无限重试。 + // ⚠️ **有上限**(2026-10-20 收敛):暂时性错误同样受次数上限约束。 // 次数上限就是 `this.retryDelays.length`(默认 10 档 ⇒ 约 7.5 分钟窗口), // 注入空数组仍然表示「完全不重试」(既有语义一字未改)。 attempt < this.retryDelays.length && @@ -669,7 +669,7 @@ export class Engine { s: Math.round(delay / 1000), n: attempt + 1, // 有次数上限 ⇒ 如实告诉用户还剩几次机会。 - max: this.retryMaxAttempts + 1, + max: this.retryDelays.length + 1, }, }; flush(true); diff --git a/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs b/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs index ac948fe..25536f6 100644 --- a/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs +++ b/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs @@ -586,7 +586,7 @@ export function createMcpHandler({ team, engine }) { result = { 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' }, instructions: t('mcp.instructions'), }; break; diff --git a/plugins/minimax-ai/minimax-bot/package.json b/plugins/minimax-ai/minimax-bot/package.json index aee5aa8..9584fc9 100644 --- a/plugins/minimax-ai/minimax-bot/package.json +++ b/plugins/minimax-ai/minimax-bot/package.json @@ -1,6 +1,6 @@ { "name": "minimax-bot", - "version": "0.7.4", + "version": "0.7.5", "private": true, "type": "module", "mcode": {