Repository navigation
fix(minimax-bot): 回合重试从 2 次延长到 10 档(仍有上限),400/404/422 不重试 #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MyPrototypeWhat
merged 6 commits into
MiniMax-AI:main
from
Hylouis233:feat/minimax-bot-retry-window
Oct 23, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
7e672a3
fix(minimax-bot): 回合重试从 2 次延长到 10 档(仍有上限),400/404/422 不重试
Hylouis233 9a42b99
fix(minimax-bot): 修 max 显示成 2/NaN、清掉过期的「无限重试」注释、版本号三处对齐 0.7.5
Hylouis233 97511cb
Merge main into feat/minimax-bot-retry-window; bump minimax-bot to 0.7.8
Hylouis233 9c217dd
fix(minimax-bot): veto retries on retryable:false and unknown outcome…
Hylouis233 b77e6e6
Merge main (9269f35) into fix/pr49-review-20261022
Hylouis233 71848a3
fix(minimax-bot): drop the optional long retry window, keep the two d…
Hylouis233 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| { | ||
| "name": "minimax-bot", | ||
| "version": "0.7.7", | ||
| "version": "0.7.8", | ||
| "private": true, | ||
| "type": "module", | ||
| "mcode": { | ||
|
|
||
38 changes: 38 additions & 0 deletions
38
plugins/minimax-ai/minimax-bot/tests/turn-retry-backoff.test.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // The automatic retry of a failed turn: the real exported default window and retry rule. | ||
| // The retry loop itself is driven end to end in turn-retry-veto.test.mjs. | ||
| import assert from 'node:assert/strict'; | ||
| import { test } from 'node:test'; | ||
| import { mkdtemp, rm } from 'node:fs/promises'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import { Store } from '../miniapp/node/store.mjs'; | ||
| import { MemoryStore } from '../miniapp/node/memory.mjs'; | ||
| import { Engine, NO_RETRY_REASONS, TURN_RETRY_DELAYS_MS, mayRetryFailure } from '../miniapp/node/engine.mjs'; | ||
|
|
||
| test('the default window is two retries, after 8 s and 20 s, and the engine uses it', async (t) => { | ||
| assert.deepEqual([...TURN_RETRY_DELAYS_MS], [8000, 20000]); | ||
| assert.ok(Object.isFrozen(TURN_RETRY_DELAYS_MS)); | ||
| const dir = await mkdtemp(join(tmpdir(), 'bot-turn-retry-default-')); | ||
| t.after(() => rm(dir, { recursive: true, force: true })); | ||
| const engine = new Engine({ store: new Store(dir), memory: new MemoryStore(dir), logger: { warn() {} }, broadcast() {} }); | ||
| assert.equal(engine.retryDelays, TURN_RETRY_DELAYS_MS); | ||
| engine.dispose(); | ||
| }); | ||
|
|
||
| test('mayRetryFailure: retryable:false, AGENT_OUTCOME_UNKNOWN and unfixable reasons always veto', () => { | ||
| for (const thrown of [true, false]) { | ||
| assert.equal(mayRetryFailure({ thrown, retryable: false }), false); | ||
| assert.equal(mayRetryFailure({ thrown, retryable: true, code: 'AGENT_OUTCOME_UNKNOWN' }), false); | ||
| assert.equal(mayRetryFailure({ thrown, code: 'AGENT_OUTCOME_UNKNOWN' }), false); | ||
| for (const reason of NO_RETRY_REASONS) { | ||
| assert.equal(mayRetryFailure({ thrown, retryable: true, reason }), false, reason); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| test('mayRetryFailure: a thrown error needs retryable:true; a returned error result needs no flag', () => { | ||
| assert.equal(mayRetryFailure({ thrown: true }), false); | ||
| assert.equal(mayRetryFailure({ thrown: true, retryable: true, code: 'AGENT_UNAVAILABLE' }), true); | ||
| assert.equal(mayRetryFailure({ thrown: false, code: 'TURN_FAILED' }), true); | ||
| assert.equal(mayRetryFailure({ thrown: false, retryable: true }), true); | ||
| }); |
118 changes: 118 additions & 0 deletions
118
plugins/minimax-ai/minimax-bot/tests/turn-retry-veto.test.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| // Engine-level checks of when a failed turn is retried automatically. Every case drives the real | ||
| // `Engine#runBotTurn` with a fake Agent and counts how many times the Agent was invoked. | ||
| import assert from 'node:assert/strict'; | ||
| import { test } from 'node:test'; | ||
| import { mkdtemp, rm } from 'node:fs/promises'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import { Store } from '../miniapp/node/store.mjs'; | ||
| import { MemoryStore } from '../miniapp/node/memory.mjs'; | ||
| import { Engine } from '../miniapp/node/engine.mjs'; | ||
| import { setLocale } from '../miniapp/node/i18n.mjs'; | ||
|
|
||
| /** Three short retries: a turn that is retried every time runs 4 times. */ | ||
| const SHORT_DELAYS = [1, 1, 1]; | ||
|
|
||
| async function fixture(t, run) { | ||
| const dir = await mkdtemp(join(tmpdir(), 'bot-turn-retry-')); | ||
| const store = new Store(dir); | ||
| const memory = new MemoryStore(dir); | ||
| await store.init(); | ||
| await memory.init(); | ||
| setLocale('en'); | ||
| const engine = new Engine({ store, memory, logger: { warn() {} }, broadcast() {}, retryDelaysMs: SHORT_DELAYS }); | ||
| const calls = { count: 0 }; | ||
| engine.agent = { | ||
| async run(input, options) { | ||
| calls.count += 1; | ||
| return run(calls.count, input, options); | ||
| }, | ||
| }; | ||
| t.after(async () => { | ||
| engine.dispose(); | ||
| await store.writeChain; | ||
| await rm(dir, { recursive: true, force: true }); | ||
| }); | ||
| const bot = store.state.bots[0]; | ||
| const chatId = `bot:${bot.id}`; | ||
| const turn = () => engine.runBotTurn({ bot, chatId, text: 'Hello', kind: 'private' }); | ||
| return { store, engine, calls, turn, chatId }; | ||
| } | ||
|
|
||
| function agentError(fields) { | ||
| return Object.assign(new Error(fields.message), fields); | ||
| } | ||
|
|
||
| test('a thrown AGENT_OUTCOME_UNKNOWN with retryable:false is not retried even if its text names a timeout', async (t) => { | ||
| const { calls, turn } = await fixture(t, () => { | ||
| throw agentError({ code: 'AGENT_OUTCOME_UNKNOWN', retryable: false, message: 'connect ETIMEDOUT 10.0.0.1:443' }); | ||
| }); | ||
| const result = await turn(); | ||
| assert.equal(calls.count, 1); | ||
| assert.equal(result.status, 'error'); | ||
| }); | ||
|
|
||
| test('a thrown error with retryable:false is not retried even if its text says "rate limit"', async (t) => { | ||
| const { calls, turn } = await fixture(t, () => { | ||
| throw agentError({ code: 'AGENT_UNAVAILABLE', retryable: false, message: 'rate limit: Token Plan quota' }); | ||
| }); | ||
| await turn(); | ||
| assert.equal(calls.count, 1); | ||
| }); | ||
|
|
||
| test('a thrown AGENT_OUTCOME_UNKNOWN is never retried, even when it claims to be retryable', async (t) => { | ||
| const { calls, turn } = await fixture(t, () => { | ||
| throw agentError({ code: 'AGENT_OUTCOME_UNKNOWN', retryable: true, message: 'Abort not confirmed' }); | ||
| }); | ||
| await turn(); | ||
| assert.equal(calls.count, 1); | ||
| }); | ||
|
|
||
| test('an error result with retryable:false is not retried even if its text names a 5xx', async (t) => { | ||
| const { calls, turn } = await fixture(t, (n, input, options) => { | ||
| options.onEvent({ type: 'turn.started', sessionId: 's1', turnId: `t${n}` }); | ||
| return { sessionId: 's1', turnId: `t${n}`, status: 'error', error: { code: 'LLM_UPSTREAM_ERROR', message: 'Internal server error (500)', retryable: false } }; | ||
| }); | ||
| await turn(); | ||
| assert.equal(calls.count, 1); | ||
| }); | ||
|
|
||
| test('an error result coded AGENT_OUTCOME_UNKNOWN is not retried', async (t) => { | ||
| const { calls, turn } = await fixture(t, (n, input, options) => { | ||
| options.onEvent({ type: 'turn.started', sessionId: 's1', turnId: `t${n}` }); | ||
| return { sessionId: 's1', turnId: `t${n}`, status: 'error', error: { code: 'AGENT_OUTCOME_UNKNOWN', message: 'Outcome unknown' } }; | ||
| }); | ||
| await turn(); | ||
| assert.equal(calls.count, 1); | ||
| }); | ||
|
|
||
| test('a thrown retryable error is retried, and the retry stops at the first success', async (t) => { | ||
| const { calls, turn, store, chatId } = await fixture(t, (n, input, options) => { | ||
| if (n < 3) throw agentError({ code: 'AGENT_UNAVAILABLE', retryable: true, message: 'Unavailable' }); | ||
| options.onEvent({ type: 'turn.started', sessionId: 's1', turnId: `t${n}` }); | ||
| return { sessionId: 's1', turnId: `t${n}`, status: 'finished', text: 'Done' }; | ||
| }); | ||
| const result = await turn(); | ||
| assert.equal(calls.count, 3); | ||
| assert.equal(result.status, 'done'); | ||
| assert.equal((await store.getMessages(chatId)).at(-1).text, 'Done'); | ||
| }); | ||
|
|
||
| test('a failed result with no retry verdict is retried until the window runs out', async (t) => { | ||
| const { calls, turn, store, chatId } = await fixture(t, (n, input, options) => { | ||
| options.onEvent({ type: 'turn.started', sessionId: 's1', turnId: `t${n}` }); | ||
| return { sessionId: 's1', turnId: `t${n}`, status: 'error', error: { code: 'TURN_FAILED', message: 'The Agent turn failed.' } }; | ||
| }); | ||
| await turn(); | ||
| assert.equal(calls.count, SHORT_DELAYS.length + 1); | ||
| assert.equal((await store.getMessages(chatId)).at(-1).retried, SHORT_DELAYS.length); | ||
| }); | ||
|
|
||
| test('a failed result whose reason a retry cannot fix is not retried', async (t) => { | ||
| const { calls, turn } = await fixture(t, (n, input, options) => { | ||
| options.onEvent({ type: 'turn.started', sessionId: 's1', turnId: `t${n}` }); | ||
| return { sessionId: 's1', turnId: `t${n}`, status: 'error', error: { code: 'LLM_QUOTA', message: 'rate limit: quota used up', reason: 'quota' } }; | ||
| }); | ||
| await turn(); | ||
| assert.equal(calls.count, 1); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit (non-blocking): consider building the failure descriptor once before
canRetry, e.g.const verdict = thrown ? {...} : {...};thenmayRetryFailure(verdict). It reads more easily than an inline ternary inside the boolean chain.