Skip to content

Commit 013eca9

Browse files
committed
fix(minimax-bot): 失败的回合说清是哪一种失败
Agent 没能启动时不再只说暂时不可用,而是说清这一轮没跑、下一步该试什么。读不懂原因的宿主错误改用插件自己的句子,不再把宿主原文带进聊天。
1 parent 2c78f99 commit 013eca9

8 files changed

Lines changed: 222 additions & 9 deletions

File tree

‎plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"schemaVersion": 1,
33
"name": "minimax-bot",
44
"displayName": "MiniMax Bot",
5-
"version": "0.7.24",
5+
"version": "0.7.25",
66
"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.",
77
"author": "MiniMax Code",
88
"icon": "icon.png",

‎plugins/minimax-ai/minimax-bot/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ The hand-over between two Bots of a group carried the tail of the previous reply
8585
- A `state.json` or a chat file that cannot be parsed no longer keeps the Mini App from starting. The unreadable file is kept as `*.corrupt-<time>` instead of being thrown away, and the store starts from the defaults: an unreadable chat starts empty, the other chats still load. The file was written with a rename, but nothing was flushed before it, so a power loss could leave an empty file behind; the bytes are flushed before the rename now, and the directory after it where the platform allows it.
8686
- A failed ledger write no longer fails the turn that already ran. The reply keeps its outcome, a group round goes on to the next member, and a finished background job still saves its status and delivers its report. The miss goes to the log.
8787

88+
## 0.7.25 A failed turn that says what happened
89+
90+
- A turn whose Agent never started says so, says that the turn did not run, and names the next step: try again, or restart MiniMax Code. It used to say only that the Agent was temporarily unavailable, which read the same as a busy model. The server and the page carry the same sentence for it, so a chat and a stored message say the same thing in both languages.
91+
- A failure whose cause the plugin cannot read — a Host error it has no sentence for — now has a sentence of its own: the turn did not finish and the reason was not identified. Where the Host throws that error, the text it came with stays out of the chat, so a session id or a service name is not shown to the user. A Host that reports the failure as a result keeps showing the text it sent.
92+
- The errors the plugin raises itself keep the message they were built with, and every code it knows keeps its own sentence. No code was added or renamed, and nothing is retried differently: only the words a failed turn shows changed.
93+
8894
## 0.7.10 Optional group notes
8995

9096
- A group can keep a note of what it said. After every round the group records the closing lines of the replies, and every Bot of the group gets that record in its standing brief.

‎plugins/minimax-ai/minimax-bot/miniapp/client/strings-app.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export default {
276276
'notice.failed': '出错了:{error}',
277277
'agentError.BUSY': '同时运行的 Bot 太多了,请稍后再试',
278278
'agentError.NOT_PERMITTED': 'Mini App 没有运行 Agent 的权限',
279-
'agentError.AGENT_UNAVAILABLE': 'Agent 暂时不可用,请稍后再试',
279+
'agentError.AGENT_UNAVAILABLE': 'Agent 没能启动,这一轮没有开始;稍后再试,或重启 MiniMax Code',
280280
'agentError.AGENT_OUTCOME_UNKNOWN': '无法确认这一轮是否已完成,请先查看会话再决定是否重发',
281281
'agentError.REQUEST_CANCELLED': '已取消',
282282
'agentError.SESSION_NOT_FOUND': '会话不存在',
@@ -543,7 +543,7 @@ export default {
543543
'notice.failed': 'Something went wrong: {error}',
544544
'agentError.BUSY': 'Too many Bots are running at once. Try again shortly',
545545
'agentError.NOT_PERMITTED': "The Mini App isn't allowed to run Agents",
546-
'agentError.AGENT_UNAVAILABLE': 'The Agent is temporarily unavailable. Try again shortly',
546+
'agentError.AGENT_UNAVAILABLE': "The Agent didn't start, so this turn didn't run. Try again later, or restart MiniMax Code",
547547
'agentError.AGENT_OUTCOME_UNKNOWN': "Couldn't confirm whether this turn finished. Check the session before resending",
548548
'agentError.REQUEST_CANCELLED': 'Cancelled',
549549
'agentError.SESSION_NOT_FOUND': 'The session no longer exists',

‎plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { stat } from 'node:fs/promises';
44
import { join } from 'node:path';
55

66
import { t } from './i18n.mjs';
7-
import { storageBudget } from './memory.mjs';
7+
import { MemoryError, storageBudget } from './memory.mjs';
88
import { selectMemoryContext } from './memory-context.mjs';
99
import { UserInbox } from './user-inbox.mjs';
1010
import { TaskDispatcher } from './task-dispatcher.mjs';
@@ -2041,5 +2041,9 @@ export function describeAgentError(error) {
20412041
const key = `agentError.${code}`;
20422042
const text = code ? t(key) : key;
20432043
if (text !== key) return text;
2044-
return errorText(error) || t('agentError.unknown');
2044+
// The plugin's own errors carry a message that was written to be read by the user; the Host's
2045+
// carry an internal identifier nobody can act on. Showing the first and hiding the second
2046+
// keeps a session id or a service name out of the chat.
2047+
if (error instanceof EngineError || error instanceof MemoryError) return errorText(error);
2048+
return t('agentError.hostError');
20452049
}

‎plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ export function createMcpHandler({ team, engine }) {
662662
result = {
663663
protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0],
664664
capabilities: { tools: { listChanged: false } },
665-
serverInfo: { name: 'minimax-bot', version: '0.7.24' },
665+
serverInfo: { name: 'minimax-bot', version: '0.7.25' },
666666
instructions: t('mcp.instructions'),
667667
};
668668
break;

‎plugins/minimax-ai/minimax-bot/miniapp/node/strings.mjs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,13 @@ export default {
180180
'err.relayTooDeep': 'Bot 之间的转交层数太深了,请直接完成当前工作',
181181
'agentError.BUSY': '同时运行的 Bot 太多了,请稍后再试',
182182
'agentError.NOT_PERMITTED': 'Mini App 没有运行 Agent 的权限',
183-
'agentError.AGENT_UNAVAILABLE': 'Agent 暂时不可用,请稍后再试',
183+
'agentError.AGENT_UNAVAILABLE': 'Agent 没能启动,这一轮没有开始;稍后再试,或重启 MiniMax Code',
184184
'agentError.AGENT_OUTCOME_UNKNOWN': '无法确认这一轮是否已完成,请先查看会话再决定是否重发',
185185
'agentError.REQUEST_CANCELLED': '已取消',
186186
'agentError.SESSION_NOT_FOUND': '会话不存在',
187187
'agentError.INVALID_ARGUMENTS': '答案格式不正确,请检查后重新提交',
188188
'agentError.unknown': '未知错误',
189+
'agentError.hostError': '这一轮没能完成,原因没能确认;稍后再试一次',
189190
'agentError.turnFailed': '这一轮没跑完,多半是模型服务繁忙或报错。点「重试」再来一次;经常出现的话,在 Bot 设置里给它换个模型',
190191
'agentError.reason.rate_limited': '模型服务现在太忙了(限流),暂时接不上。点「重试」再来一次,或在 Bot 设置里换个模型',
191192
'agentError.reason.overloaded': '模型服务过载,暂时接不上。点「重试」再来一次,或在 Bot 设置里换个模型',
@@ -502,12 +503,13 @@ export default {
502503
'err.relayTooDeep': 'Too many hand-offs between Bots. Finish the current work directly',
503504
'agentError.BUSY': 'Too many Bots are running at once. Try again shortly',
504505
'agentError.NOT_PERMITTED': "The Mini App isn't allowed to run Agents",
505-
'agentError.AGENT_UNAVAILABLE': 'The Agent is temporarily unavailable. Try again shortly',
506+
'agentError.AGENT_UNAVAILABLE': "The Agent didn't start, so this turn didn't run. Try again later, or restart MiniMax Code",
506507
'agentError.AGENT_OUTCOME_UNKNOWN': "Couldn't confirm whether this turn finished. Check the session before resending",
507508
'agentError.REQUEST_CANCELLED': 'Cancelled',
508509
'agentError.SESSION_NOT_FOUND': 'The session no longer exists',
509510
'agentError.INVALID_ARGUMENTS': 'The answer was rejected. Check it and submit again',
510511
'agentError.unknown': 'Unknown error',
512+
'agentError.hostError': "This turn didn't finish and the reason wasn't identified. Try again later",
511513
'agentError.turnFailed': "This turn didn't finish, most likely because the model service was busy or returned an error. Press Retry; if it keeps happening, pick another model in the Bot's settings",
512514
'agentError.reason.rate_limited': "The model service is too busy right now (rate limited). Press Retry, or pick another model in the Bot's settings",
513515
'agentError.reason.overloaded': "The model service is overloaded right now. Press Retry, or pick another model in the Bot's settings",

‎plugins/minimax-ai/minimax-bot/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "minimax-bot",
3-
"version": "0.7.24",
3+
"version": "0.7.25",
44
"private": true,
55
"type": "module",
66
"mcode": {
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// @ts-check
2+
// The words a failed turn shows the user, and the keys behind them.
3+
//
4+
// A turn that died for a reason the plugin cannot read used to say "try again shortly": no
5+
// cause, no next step, nothing the user could act on. The copy here replaces that with three
6+
// different sentences for three different situations — a code the plugin knows, an error the
7+
// plugin raised itself, and a failure whose cause only the Host knows — so the user can tell a
8+
// busy model from a session that never started.
9+
//
10+
// The page localises the codes it knows from its own dictionary, so a code the server and the
11+
// page spell differently is a sentence the user reads twice, in two languages, with two
12+
// meanings. The dictionaries are checked against each other here rather than by eye.
13+
14+
import assert from 'node:assert/strict';
15+
import { test } from 'node:test';
16+
import { mkdtemp, rm } from 'node:fs/promises';
17+
import { tmpdir } from 'node:os';
18+
import { join } from 'node:path';
19+
20+
import SERVER from '../miniapp/node/strings.mjs';
21+
import CLIENT from '../miniapp/client/strings-app.js';
22+
import { t, setLocale } from '../miniapp/node/i18n.mjs';
23+
import { describeAgentError, Engine, EngineError } from '../miniapp/node/engine.mjs';
24+
import { MemoryError, MemoryStore } from '../miniapp/node/memory.mjs';
25+
import { Store } from '../miniapp/node/store.mjs';
26+
27+
/** The sentence for a turn whose Agent never started. */
28+
const UNAVAILABLE = 'agentError.AGENT_UNAVAILABLE';
29+
/** The sentence for a failure whose cause the plugin cannot read. */
30+
const HOST_ERROR = 'agentError.hostError';
31+
/** The sentence for a model that cannot be used; it names a setting, so it is not a fallback. */
32+
const MODEL_UNAVAILABLE = 'agentError.MODEL_UNAVAILABLE';
33+
34+
/** The model sentence, written out: it may not drift while the other copy is being changed. */
35+
const MODEL_TEXT = {
36+
zh: '这个 Bot 设置的模型现在用不了(可能已停用或下线),请在 Bot 设置里换一个模型或改回默认',
37+
en: "This Bot's model can't be used right now (it may have been disabled or removed). Pick another model, or go back to the default, in the Bot's settings",
38+
};
39+
40+
/** Words that belong to the model sentence and would mislead a reader of the other one. */
41+
const MODEL_WORDS = ['模型', 'model', 'settings', 'default', 'disabled', '停用', '下线'];
42+
43+
/** The codes the plugin knows by name, in the order the page shows them. */
44+
const KNOWN_CODES = [
45+
'BUSY',
46+
'NOT_PERMITTED',
47+
'SESSION_NOT_FOUND',
48+
'INVALID_ARGUMENTS',
49+
'AGENT_OUTCOME_UNKNOWN',
50+
'REQUEST_CANCELLED',
51+
];
52+
53+
/**
54+
* A turn that fails once, with the Agent replaced by one that throws.
55+
* @param {import('node:test').TestContext} ctx
56+
* @param {unknown} thrown
57+
*/
58+
async function failedTurn(ctx, thrown) {
59+
const dir = await mkdtemp(join(tmpdir(), 'bot-agent-error-'));
60+
const store = new Store(dir);
61+
const memory = new MemoryStore(dir);
62+
await store.init();
63+
await memory.init();
64+
const engine = new Engine({
65+
store,
66+
memory,
67+
logger: { warn() {} },
68+
broadcast() {},
69+
retryDelaysMs: [],
70+
});
71+
engine.agent = {
72+
async run() {
73+
throw thrown;
74+
},
75+
};
76+
ctx.after(async () => {
77+
engine.dispose();
78+
await store.writeChain;
79+
await rm(dir, { recursive: true, force: true });
80+
});
81+
const bot = store.state.bots[0];
82+
const chatId = `bot:${bot.id}`;
83+
await engine.runBotTurn({ bot, chatId, text: 'Hello', kind: 'private' });
84+
const messages = await store.getMessages(chatId);
85+
return messages.at(-1);
86+
}
87+
test('the page and the server spell the unavailable sentence the same way', () => {
88+
assert.equal(SERVER.zh[UNAVAILABLE], CLIENT.zh[UNAVAILABLE]);
89+
assert.equal(SERVER.en[UNAVAILABLE], CLIENT.en[UNAVAILABLE]);
90+
});
91+
92+
test('every dictionary carries the same keys in both languages', () => {
93+
assert.deepEqual(Object.keys(SERVER.zh).sort(), Object.keys(SERVER.en).sort());
94+
assert.deepEqual(Object.keys(CLIENT.zh).sort(), Object.keys(CLIENT.en).sort());
95+
});
96+
97+
test('the page reads no error key the server does not have', () => {
98+
const server = new Set(Object.keys(SERVER.zh).filter((key) => key.startsWith('agentError.')));
99+
const page = Object.keys(CLIENT.zh).filter((key) => key.startsWith('agentError.'));
100+
assert.ok(page.length > 0);
101+
for (const key of page) assert.ok(server.has(key), `the page has ${key} and the server does not`);
102+
});
103+
104+
test('the server has a sentence for a cause it cannot read, in both languages', () => {
105+
for (const lang of ['zh', 'en']) {
106+
const text = SERVER[lang][HOST_ERROR];
107+
assert.ok(typeof text === 'string' && text.length > 0, `${lang} has no ${HOST_ERROR}`);
108+
assert.notEqual(text, HOST_ERROR);
109+
}
110+
});
111+
112+
test('the two new sentences differ between the two languages', () => {
113+
assert.notEqual(SERVER.zh[UNAVAILABLE], SERVER.en[UNAVAILABLE]);
114+
assert.notEqual(SERVER.zh[HOST_ERROR], SERVER.en[HOST_ERROR]);
115+
});
116+
117+
test('the unavailable sentence does not borrow the model sentence', () => {
118+
for (const lang of ['zh', 'en']) {
119+
assert.notEqual(SERVER[lang][UNAVAILABLE], SERVER[lang][MODEL_UNAVAILABLE]);
120+
const lower = SERVER[lang][UNAVAILABLE].toLowerCase();
121+
for (const word of MODEL_WORDS) {
122+
assert.ok(!lower.includes(word.toLowerCase()), `the ${lang} unavailable sentence mentions ${word}`);
123+
}
124+
}
125+
});
126+
127+
test('every new sentence stays short, alone and behind the failure prefix', () => {
128+
for (const lang of ['zh', 'en']) {
129+
for (const key of [UNAVAILABLE, HOST_ERROR]) {
130+
const text = SERVER[lang][key];
131+
assert.ok(text.length <= 120, `${lang} ${key} is ${text.length} characters`);
132+
const shown = t('notice.failed', { error: text }, lang);
133+
assert.ok(shown.length <= 120, `${lang} ${key} under notice.failed is ${shown.length} characters`);
134+
}
135+
}
136+
});
137+
138+
test('the model sentence is the one that was there before', () => {
139+
assert.equal(SERVER.zh[MODEL_UNAVAILABLE], MODEL_TEXT.zh);
140+
assert.equal(SERVER.en[MODEL_UNAVAILABLE], MODEL_TEXT.en);
141+
});
142+
143+
test('a Host error with a cause the plugin cannot read is described without its text', () => {
144+
for (const lang of ['zh', 'en']) {
145+
setLocale(lang);
146+
const text = describeAgentError({ code: 'SessionServiceError', message: 'SessionServiceError: svc-abc123 bind failed' });
147+
assert.equal(text, t(HOST_ERROR));
148+
for (const leak of ['svc-abc123', 'SessionServiceError', 'bind']) {
149+
assert.ok(!text.includes(leak), `the sentence leaks ${leak}`);
150+
}
151+
}
152+
});
153+
154+
test('a failure that is not an error at all is described without its text', () => {
155+
for (const lang of ['zh', 'en']) {
156+
setLocale(lang);
157+
for (const thrown of [null, undefined, 'boom', {}]) {
158+
const text = describeAgentError(thrown);
159+
assert.equal(text, t(HOST_ERROR));
160+
for (const leak of ['null', 'undefined', 'boom']) {
161+
assert.ok(!text.includes(leak), `the sentence leaks ${leak}`);
162+
}
163+
}
164+
}
165+
});
166+
167+
test("the plugin's own errors keep the message they were built with", () => {
168+
for (const lang of ['zh', 'en']) {
169+
setLocale(lang);
170+
const expected = t('err.noAgentSupport', undefined, lang);
171+
assert.equal(describeAgentError(new EngineError('agent_unavailable', expected)), expected);
172+
assert.equal(describeAgentError(new MemoryError(expected)), expected);
173+
}
174+
});
175+
176+
test('a known code keeps its own sentence in both languages', () => {
177+
for (const lang of ['zh', 'en']) {
178+
setLocale(lang);
179+
for (const code of KNOWN_CODES) {
180+
const key = `agentError.${code}`;
181+
assert.equal(describeAgentError({ code }), t(key));
182+
}
183+
}
184+
});
185+
186+
test('a turn whose Agent never started shows the unavailable sentence and hides the cause', async (ctx) => {
187+
setLocale('en');
188+
const message = await failedTurn(ctx, { code: 'AGENT_UNAVAILABLE', message: 'SessionServiceError: svc-abc123 bind failed' });
189+
assert.equal(message.errorCode, 'AGENT_UNAVAILABLE');
190+
assert.equal(message.error, t('agentError.AGENT_UNAVAILABLE'));
191+
assert.ok(!message.error.includes('svc-abc123'), 'the error keeps the cause');
192+
assert.ok(!String(message.text ?? '').includes('svc-abc123'), 'the text keeps the cause');
193+
});
194+
195+
test('a turn that failed for a reason the plugin cannot name shows the host sentence', async (ctx) => {
196+
setLocale('en');
197+
const message = await failedTurn(ctx, { code: 'SessionServiceError' });
198+
assert.equal(message.errorCode, 'SessionServiceError');
199+
assert.equal(message.error, t(HOST_ERROR));
200+
assert.ok(!message.error.includes('SessionServiceError'), 'the error keeps the code');
201+
});

0 commit comments

Comments
 (0)