Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"name": "minimax-bot",
"displayName": "MiniMax Bot",
"version": "0.7.21",
"version": "0.7.23",
"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",
Expand Down
4 changes: 4 additions & 0 deletions plugins/minimax-ai/minimax-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ A shutdown no longer throws away work that never started. Such a task stays queu

The usage ledger counts the time the model really ran. A retried turn waits between the attempts, and that wait was counted as the model's time: a turn that waited half a minute between two attempts was written down as a turn of half a minute, and the panel averaged that up. The duration is now the time the model ran and the panel's average follows it; the wait between the attempts is kept beside it in the ledger (`usage.jsonl`), not inside the duration. A turn the model never ran has no duration to report, so it is left out of the average instead of counting as zero seconds.

## 0.7.23 The hand-over carries the conclusions of the round

The hand-over between two Bots of a group carried the tail of the previous reply: the last thing one Bot said, not what the round had concluded — and a discussion concludes over several replies, not in the closing line of one of them. The hand-over now carries the conclusions of the round so far, one entry per hop and each with the Bot that said it, the way a seminar hands its conclusion to the next speaker instead of the last sentence that was said. A round with a single hop hands over exactly what it handed over before, byte for byte. The note setting does not change any of this: the hand-over reads the round itself, and a round is built whether or not the group keeps a note. The note stays what it always was, a record of the earlier rounds that lives in the standing brief of every Bot.

## 0.7.10 Optional group notes

- 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export default {
'panels.group.relayLimit': '接力跳数上限',
'panels.group.relayLimitHint': '0 表示不限:接力链可以一直跑下去。填上数字后(最多 20),这个群里一轮接力最多几个 Bot 说话;到上限时群里会说明本轮收口。「所有人回复」的群里,这个上限连初始成员一起算。',
'panels.group.handover': '把上一位的结论交给下一位',
'panels.group.handoverHint': '下一位的提示词里会带上上一位回复的结论(就是它的尾部),省得下一位再翻一遍工具。上一位的原话本来就在「新消息」里,这里是再强调一遍结论。只多一段提示词,不多花回合。',
'panels.group.handoverHint': '下一位的提示词里会带上本轮到目前为止各 Bot 的结论:每个说过的 Bot 一条,前面标着是谁说的;一轮里只有一个人说过时,就是他回复的尾部。带的是结论不是原话(原话本来就在「新消息」里),也只是记录,不代表群里已经达成共识。只多一段提示词,不多花回合。',
'panels.group.notesHeading': '群纪要',
'panels.group.notes': '把本群说过的话记下来',
'panels.group.notesHint': '每轮结束后,群会把各 Bot 回复的结尾记成一份纪要,写在群资料里:清空聊天记录(新建对话)之后纪要还在,新加入的 Bot 也能看到。关掉开关只是不再记、也不再给 Bot 看,记下的内容会保留到你清空为止。纪要只是记录,不代表群里已经达成共识。只多一段提示词,不多花回合。',
Expand Down Expand Up @@ -544,7 +544,7 @@ export default {
'panels.group.relayLimit': 'Relay hop limit',
'panels.group.relayLimitHint': '0 means no limit: the relay chain can keep going. With a number (20 at most), at most that many Bots speak in one chain in this group; when the limit is reached the group says so and the round wraps up. In a group where every member replies, the limit counts those first replies too.',
'panels.group.handover': 'Hand the previous conclusion to the next Bot',
'panels.group.handoverHint': "The next Bot's prompt carries the conclusion of the previous reply (its tail), so the next Bot does not have to dig the same ground out of the tools again. The previous reply is in the new messages anyway — this repeats the conclusion on purpose. It is prompt text only — no extra turn.",
'panels.group.handoverHint': "The next Bot's prompt carries what the Bots of the round concluded so far: one entry per Bot, tagged with who said it, and the tail of that reply when only one Bot spoke. It carries the conclusions, not the words themselves — those are in the new messages anyway — and it is a record, not an agreement the group reached. It is prompt text only — no extra turn.",
'panels.group.notesHeading': 'Group notes',
'panels.group.notes': 'Keep a record of what this group said',
'panels.group.notesHint': 'After every round the group records the closing lines of the replies as a note, kept with the group settings: the note is still there after the chat was cleared (Start new chat), and a Bot that joins later sees it too. Switching the note off stops the recording and stops handing it to the Bots, but what was recorded stays until you clear it. A note is a record, not an agreement the group reached. It is prompt text only — no extra turn.',
Expand Down
36 changes: 21 additions & 15 deletions plugins/minimax-ai/minimax-bot/miniapp/node/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { selectMemoryContext } from './memory-context.mjs';
import { UserInbox } from './user-inbox.mjs';
import { TaskDispatcher } from './task-dispatcher.mjs';
import { modelKey, newId, withEffort } from './store.mjs';
import { CONVERGE_MARK, CONTEXT_BLOCK_END, CONTEXT_BLOCK_MARK, handoverRequested, handoverSummary, hopLimit, neutralizeMarks, renderHandover } from './relay-guard.mjs';
import { CONVERGE_MARK, CONTEXT_BLOCK_END, CONTEXT_BLOCK_MARK, handoverRequested, handoverSummary, hopLimit, neutralizeMarks, renderHandover, roundEntries } from './relay-guard.mjs';
import { appendNote, notesEnabled, renderNote } from './group-note.mjs';

/** @typedef {import('./miniapp-api.js').MiniAppAgentClient} MiniAppAgentClient */
Expand Down Expand Up @@ -297,7 +297,7 @@ export class Engine {
}

/**
* Whether the conclusion of the previous hop travels with the next hop's prompt.
* Whether the conclusions of the round travel with the next hop's prompt.
* @param {any} group
* @returns {boolean}
*/
Expand All @@ -316,23 +316,29 @@ export class Engine {
}

/**
* The conclusion of the previous hop, handed over to the next one.
* The conclusions of the round so far, handed over to the next one.
*
* Free by construction: the summary is text that rides along with the next turn — no model
* turn, no tool call, no memory write. The note the group already produced wins; without a
* note the tail of the previous reply is used.
* turn, no tool call, no memory write.
*
* The round is what the group itself produced in this round, so it is the right thing to hand
* over: one entry per hop, each with the Bot that said it. The group's own note is not part of
* it — that note holds the earlier rounds and lives in the standing brief.
*
* @param {any} group
* @param {{ botId: string, name: string, text: string, ts: number } | undefined} last
* @param {{ speaker: string, text: string }[]} round what the Bots of this round said so far
* @returns {{ from: string, text: string } | undefined}
*/
handoverFor(group, last) {
handoverFor(group, round) {
if (!this.handoverEnabled(group)) return undefined;
if (!last?.text) return undefined;
// This build keeps no group notes, so the hand-over carries the tail of the reply.
const text = handoverSummary({ note: '', reply: last.text });
const text = handoverSummary({ round });
if (!text) return undefined;
return { from: clean(last.name), text };
// The entries that count are the entries the summary shows: an empty entry is not a
// conclusion. One real conclusion is one voice, and the block may name it; several are
// several Bots, and the block must not put them in one mouth.
const entries = roundEntries(round);
const from = entries.length === 1 ? clean(entries[0].speaker) : '';
return { from, text };
}


Expand Down Expand Up @@ -1315,9 +1321,9 @@ export class Engine {
const bot = this.store.getBot(botId);
const fresh = this.store.getGroup(group.id);
if (!bot || !fresh || !fresh.memberIds.includes(botId)) continue;
// The conclusion of the previous hop rides along with this turn. It is prompt text, not
// a model turn: the hand-over costs nothing.
const handover = this.handoverFor(fresh, lastConclusion);
// What the Bots of this round concluded so far rides along with this turn. It is prompt
// text, not a model turn: the hand-over costs nothing.
const handover = this.handoverFor(fresh, noteRound);
hops += 1;
const result = await this.runBotTurn({ bot, chatId, text: '', kind: 'group', sourceMessageId, sourceMessageIds, signal: options.signal, taskId: options.taskId, handover });
lastResult = result;
Expand Down Expand Up @@ -1388,7 +1394,7 @@ export class Engine {

async groupPrompt(group, bot, { handover } = {}) {
const chatId = `group:${group.id}`;
// The conclusion of the previous hop: text that rides along with this turn, no model turn.
// The conclusions of the round: text that rides along with this turn, no model turn.
const hand = handover ? renderHandover(handover) : '';
const head = [];
if (hand) head.push(hand, '');
Expand Down
37 changes: 8 additions & 29 deletions plugins/minimax-ai/minimax-bot/miniapp/node/group-note.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// Everything in this module is pure: the engine decides *when* the note is written and where it
// goes, this module only says *what* it says. That keeps the rules testable without a model.

import { neutralizeMarks } from './relay-guard.mjs';
import { ENTRY_MAX, SPEAKER_MAX, neutralizeMarks, tailOf as cutTail } from './relay-guard.mjs';
import { t } from './i18n.mjs';

/**
Expand All @@ -36,10 +36,10 @@ export const GROUP_NOTE_END = '</minimax-bot-group-summary>';
export const NOTE_MAX_ENTRIES = 6;
/** How many characters the whole note may take. */
export const NOTE_MAX_CHARS = 1200;
/** How many characters one entry may take. */
export const NOTE_ENTRY_MAX = 240;
/** How many characters the name of a speaker may take. */
export const NOTE_SPEAKER_MAX = 60;
/** How many characters one entry may take. The same budget the hand-over uses, defined once in `relay-guard.mjs`. */
export const NOTE_ENTRY_MAX = ENTRY_MAX;
/** How many characters the name of a speaker may take. Defined once in `relay-guard.mjs`. */
export const NOTE_SPEAKER_MAX = SPEAKER_MAX;

/**
* Whether the group keeps a note.
Expand Down Expand Up @@ -71,39 +71,18 @@ function cleanEntry(entry) {
return { speaker: speaker.slice(0, NOTE_SPEAKER_MAX), text: tailOf(text, NOTE_ENTRY_MAX) };
}

/**
* A word character in the Latin sense: a letter or a digit that a cut can land inside.
*
* A CJK ideograph is its own unit, so a cut between two of them already lands on a boundary.
*/
const LATIN_WORD = /[A-Za-z0-9_]/;

/** How much of a broken word may be dropped to reach the next boundary. */
const WORD_PREFIX_MAX = 20;

/**
* The closing part of a text, cut on a word boundary.
*
* A plain cut can land inside a word — or inside the half of a tag the neutralising left
* behind — and the entry would start mid-word. Only a Latin word can be cut in half: Chinese
* has no spaces, so trimming up to the first whitespace there would drop the whole entry.
* The cut itself lives in `relay-guard.mjs`: that module cannot import this one, and both need
* the same rule — a plain cut would land inside a word and the entry would start mid-word.
*
* @param {string} text
* @param {number} [max]
* @returns {string}
*/
export function tailOf(text, max = NOTE_ENTRY_MAX) {
const value = String(text ?? '');
if (value.length <= max) return value;
const cut = value.length - max;
const tail = value.slice(cut);
// The characters around the cut tell whether it landed inside a Latin word.
if (!LATIN_WORD.test(value[cut - 1] ?? '') || !LATIN_WORD.test(tail[0] ?? '')) return tail;
// Drop the rest of that word and the space after it. A word longer than this is left alone:
// dropping half of a long token would cost more than the fragment is worth.
const fragment = /^[A-Za-z0-9_]+/.exec(tail)?.[0] ?? '';
if (!fragment || fragment.length > WORD_PREFIX_MAX) return tail;
return tail.slice(fragment.length).replace(/^\s+/, '') || tail;
return cutTail(text, max);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,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.21' },
serverInfo: { name: 'minimax-bot', version: '0.7.23' },
instructions: t('mcp.instructions'),
};
break;
Expand Down
Loading
Loading