Skip to content
Merged
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.10",
"version": "0.7.11",
"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
9 changes: 8 additions & 1 deletion plugins/minimax-ai/minimax-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ Copy this directory, including the hidden `.minimax-plugin/` directory, into the
- It is prompt text: the note is written from the replies the round already produced, so it costs no extra Agent turn, no extra tool call and no memory write.
- The note is a record, not an agreement. It says what the Bots said at the end of earlier rounds, and every Bot of the group is told exactly that in the prompt.
- The note is off unless you switch it on in the group settings, and a group without a note gets exactly the prompt it got before. Only you can set it: a Bot cannot switch it on, and the record itself is written by the plugin — a Bot cannot write into it.
- The note holds the most recent rounds and stays within a small size limit. The group settings show what was recorded, and you can clear it there.
- The note holds the most recent rounds and stays within a small size limit. The group settings show what was recorded, and you can clear it there. Switching the note off stops the recording and stops handing the record to the Bots, but what was already recorded stays until you clear it.

## 0.7.11 The group note, cleaned up

- Switching the note off no longer hides what was recorded. The group settings keep showing the record and its **Clear** button while the record exists, and say that the switch is off. The record itself is kept until you clear it, so switching the note back on hands the same record to the Bots again.
- The panel shows the same list the Bots read: the cleaned and bounded entries, not whatever the state file happens to hold.
- The protocol marks are now matched case-insensitively, and the marks of the standing-context block join the list of marks a reply may not forge. A reply that ends with such a tag can no longer close the context block early.
- A recorded line is cut on a word boundary, so it no longer starts in the middle of a word.

## 0.7.9 Optional relay guardrails for groups

Expand Down
5 changes: 4 additions & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/client/panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,12 @@ function groupOverview(group) {
h('span', null, t('panels.group.notes')),
),
h('p', { class: 'hint', style: { marginTop: '8px' } }, t('panels.group.notesHint')),
...(group.notes === true && Array.isArray(group.noteEntries) && group.noteEntries.length
// The record stays visible while it exists, even with the switch off: what was recorded
// has to be readable and clearable, otherwise turning the switch off would hide it.
...(Array.isArray(group.noteEntries) && group.noteEntries.length
? [
h('p', { class: 'hint', style: { marginTop: '8px' } }, t('panels.group.notesList')),
...(group.notes === true ? [] : [h('p', { class: 'hint' }, t('panels.group.notesKept'))]),
...group.noteEntries.map((entry) => clipped(`[${entry.speaker}] ${entry.text}`)),
h(
'button',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ export default {
'panels.group.handoverHint': '下一位的提示词里会带上上一位回复的结论(就是它的尾部),省得下一位再翻一遍工具。上一位的原话本来就在「新消息」里,这里是再强调一遍结论。只多一段提示词,不多花回合。',
'panels.group.notesHeading': '群纪要',
'panels.group.notes': '把本群说过的话记下来',
'panels.group.notesHint': '每轮结束后,群会把各 Bot 回复的结尾记成一份纪要,写在群资料里:清空聊天记录(新建对话)之后纪要还在,新加入的 Bot 也能看到。纪要只是记录,不代表群里已经达成共识。只多一段提示词,不多花回合。',
'panels.group.notesHint': '每轮结束后,群会把各 Bot 回复的结尾记成一份纪要,写在群资料里:清空聊天记录(新建对话)之后纪要还在,新加入的 Bot 也能看到。纪要只留最近几轮(最多 6 条 / 1200 字),更早的会掉出去;关掉开关只是不再记、也不再给 Bot 看,记下的内容会保留到你清空为止。纪要只是记录,不代表群里已经达成共识。只多一段提示词,不多花回合。',
'panels.group.notesList': '已记录的纪要:',
'panels.group.notesKept': '开关关着,这份纪要保留着,但不再记录、也不再给 Bot 看。',
'panels.group.notesClear': '清空纪要',
'panels.group.settingsKept': '成员设置保留',
'panels.group.needMember': '至少选一个成员',
Expand Down Expand Up @@ -543,8 +544,9 @@ export default {
'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.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. A note is a record, not an agreement the group reached. It is prompt text only — no extra turn.',
'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. Only the last rounds are kept (6 entries and 1200 characters at most); older ones drop off. 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.',
'panels.group.notesList': 'Recorded so far:',
'panels.group.notesKept': 'The switch is off, so this record is kept but neither added to nor read by the Bots.',
'panels.group.notesClear': 'Clear the note',
'panels.group.settingsKept': 'Member settings are kept',
'panels.group.needMember': 'Pick at least one member',
Expand Down
6 changes: 3 additions & 3 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, handoverRequested, handoverSummary, hopLimit, renderHandover } from './relay-guard.mjs';
import { CONVERGE_MARK, CONTEXT_BLOCK_END, CONTEXT_BLOCK_MARK, handoverRequested, handoverSummary, hopLimit, renderHandover } from './relay-guard.mjs';
import { appendNote, notesEnabled, renderNote } from './group-note.mjs';

/** @typedef {import('./miniapp-api.js').MiniAppAgentClient} MiniAppAgentClient */
Expand Down Expand Up @@ -393,7 +393,7 @@ export class Engine {
// the group keeps one, so the brief of a group without notes stays byte-identical.
const note = group ? renderNote(group) : '';
const lines = [
'<minimax-bot-context>',
CONTEXT_BLOCK_MARK,
t('brief.intro', { name: bot.name, id: bot.id }),
bot.role ? t('brief.role', { role: bot.role }) : '',
bot.isMain ? t('brief.mainPower') : '',
Expand Down Expand Up @@ -442,7 +442,7 @@ export class Engine {
'',
t('brief.hReply'),
t('brief.reply'),
'</minimax-bot-context>',
CONTEXT_BLOCK_END,
];
return lines.filter((line, index, all) => !(line === '' && all[index - 1] === '')).join('\n');
}
Expand Down
24 changes: 23 additions & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/node/group-note.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,29 @@ function cleanEntry(entry) {
const speaker = neutralizeMarks(String(entry.speaker ?? '')).replace(/\s+/g, ' ').trim();
const text = neutralizeMarks(String(entry.text ?? '')).replace(/\s+/g, ' ').trim();
if (!speaker || !text) return undefined;
return { speaker: speaker.slice(0, NOTE_SPEAKER_MAX), text: text.slice(-NOTE_ENTRY_MAX) };
return { speaker: speaker.slice(0, NOTE_SPEAKER_MAX), text: tailOf(text, NOTE_ENTRY_MAX) };
}

/**
* 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. When the cut lands inside a word, the partial
* word is dropped, so the entry starts where a word starts.
*
* @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 character before the cut tells whether the cut landed inside a word.
if (!/\S/.test(value[cut - 1] ?? '')) return tail;
const trimmed = tail.replace(/^\S*\s+/, '');
return trimmed || tail;
}

/**
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 @@ -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.10' },
serverInfo: { name: 'minimax-bot', version: '0.7.11' },
instructions: t('mcp.instructions'),
};
break;
Expand Down
28 changes: 27 additions & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/node/relay-guard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export const HANDOVER_BLOCK_MARK = '<minimax-bot-handover>';
export const HANDOVER_BLOCK_END = '</minimax-bot-handover>';
/** Marks the prompt that asks the current speaker to wrap up. */
export const CONVERGE_MARK = '<minimax-bot-relay-converge>';
/**
* Opens the block that carries the Bot's standing context.
*
* The context block wraps the whole brief, so a reply that ends with its closing tag would
* close the block early and make the rest of the text look like it sits outside the context.
*/
export const CONTEXT_BLOCK_MARK = '<minimax-bot-context>';
/** Closes the block that carries the Bot's standing context. */
export const CONTEXT_BLOCK_END = '</minimax-bot-context>';
/** Longest summary handed over to the next hop. */
export const HANDOVER_MAX = 600;
/** How much of the previous reply's tail is kept when the group keeps no note. */
Expand All @@ -42,6 +51,8 @@ const FORGED_MARKS = [
HANDOVER_BLOCK_MARK,
HANDOVER_BLOCK_END,
CONVERGE_MARK,
CONTEXT_BLOCK_MARK,
CONTEXT_BLOCK_END,
'<minimax-bot-group-summary>',
'</minimax-bot-group-summary>',
'<minimax-bot-group-summary-task>',
Expand Down Expand Up @@ -82,6 +93,21 @@ export function handoverRequested(group) {
return group?.handover === true;
}

/**
* A protocol mark is a literal, so the pattern has to escape whatever it contains.
* @param {string} text
* @returns {string}
*/
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
* The marks are matched case-insensitively: a tag in another case is still a tag, and a Host
* that reads the marks case-insensitively must not be handed one.
*/
const FORGED_PATTERNS = FORGED_MARKS.map((mark) => new RegExp(escapeRegExp(mark), 'gi'));

/**
* Removes the protocol marks from a text, so a reply can never pretend to open or close a
* block of its own.
Expand All @@ -90,7 +116,7 @@ export function handoverRequested(group) {
*/
export function neutralizeMarks(text) {
let out = String(text ?? '');
for (const mark of FORGED_MARKS) out = out.split(mark).join(' ');
for (const pattern of FORGED_PATTERNS) out = out.replace(pattern, ' ');
return out;
}

Expand Down
7 changes: 6 additions & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/node/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { extname, isAbsolute, join } from 'node:path';

import { createHash } from 'node:crypto';
import { Engine, EngineError, UPLOAD_NAME_RE } from './engine.mjs';
import { noteEntries } from './group-note.mjs';
import { createMcpHandler } from './mcp.mjs';
import { LANGUAGE_PREFS, locale, matchLocale, t } from './i18n.mjs';
import { memoryBudget, MemoryError, MemoryStore } from './memory.mjs';
Expand Down Expand Up @@ -138,7 +139,11 @@ export async function start(context) {

const snapshot = () => ({
bots: store.state.bots,
groups: store.state.groups,
// A group's note is shown in the panel exactly as the Bots get it: the cleaned and bounded
// list, not whatever the state file happens to hold.
groups: store.state.groups.map((group) => (
Array.isArray(group.noteEntries) ? { ...group, noteEntries: noteEntries(group) } : group
)),
schedules: store.state.schedules,
helpers: engine.helpersList(),
tasks: engine.tasks.uiList(),
Expand Down
3 changes: 2 additions & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/node/team.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,8 @@ export class Team {
if (patch.maxHops !== undefined) draft.maxHops = hopLimit({ maxHops: patch.maxHops });
if (patch.handover !== undefined) draft.handover = Boolean(patch.handover);
// The group's note: the switch and the record are both the operator's. Switching it off
// stops the record and the hand-over; clearing it drops what was recorded.
// stops the recording and stops handing the record to the Bots; the record itself stays
// until it is cleared. (The relay hand-over has its own switch, this one does not touch it.)
if (patch.notes !== undefined) draft.notes = Boolean(patch.notes);
if (patch.clearNotes === true) draft.noteEntries = [];
if (patch.members !== undefined) {
Expand Down
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minimax-bot",
"version": "0.7.10",
"version": "0.7.11",
"private": true,
"type": "module",
"mcode": {
Expand Down
Loading
Loading