From 1d0ac9b2a292b54461432f771bfa7d954fd53ba6 Mon Sep 17 00:00:00 2001 From: Apageoflove Date: Fri, 13 Nov 2026 04:03:15 -0500 Subject: [PATCH] =?UTF-8?q?fix(minimax-bot):=20=E5=88=A0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=97=B6=E4=B8=8D=E5=86=8D=E9=A1=BA=E6=89=8B?= =?UTF-8?q?=E6=8A=8A=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95=E6=B8=85=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删 Bot、删群、Start new chat 都走 deleteChat。它先清空这条会话的 message 文件,再写 state.json 把会话忘掉。第二步失败时第一步已经落盘,接口回 500、 会话仍在侧栏,点进去是空的,再点一次也救不回来。 顺序换过来:先写不带这条会话的 state,成功了再清消息文件,两个写仍在同一个 队列槽内。内存里的 sessions 条目也等写成功才提交,避免失败时内存领先磁盘、 被后续无关保存顺手落盘。 新测试把 state.json 换成目录让写失败,断言 message 文件里那条消息还在。 未改的上游上红、切片上绿。全量 412 条通过,上游基线 411 条。 --- .../minimax-bot/.minimax-plugin/plugin.json | 2 +- plugins/minimax-ai/minimax-bot/README.md | 5 +++++ .../minimax-bot/miniapp/node/mcp.mjs | 2 +- .../minimax-bot/miniapp/node/store.mjs | 10 ++++++++-- plugins/minimax-ai/minimax-bot/package.json | 2 +- .../tests/storage-reliability.test.mjs | 20 ++++++++++++++++++- 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json b/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json index bfc05943..e08811fe 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.26", + "version": "0.7.28", "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 17ce0f5b..fd2d8c50 100644 --- a/plugins/minimax-ai/minimax-bot/README.md +++ b/plugins/minimax-ai/minimax-bot/README.md @@ -95,6 +95,11 @@ The hand-over between two Bots of a group carried the tail of the previous reply - The approval claim in **Agent turns** mixed the plugin's own behaviour with the Host's. The plugin never approves a call by itself — that part was true and stays true — but whether approval is asked for at all is decided by the Host's permission mode, and a Host that never asks for approval never shows the card. The old sentence stated the first half as if it also settled the second, so it read as a promise about an outcome the plugin does not control. It now names the Host condition, and keeps the plugin's own side: when approval is needed, the plugin never approves it by itself. +## 0.7.28 A reset that cannot be saved keeps the transcript + +- **Start new chat**, deleting a Bot and deleting a group all empty the transcript and then write the state that forgets the chat. When that state write failed, the empty transcript was already on disk: the request reported a failure, the chat was still listed, and its history was gone for good. +- The state is now written first, and the in-memory session entry is dropped only after that write succeeded. A failure leaves the chat, its messages and its session cursor as they were, so the same action can simply be tried again. + ## 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. diff --git a/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs b/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs index 884b4cc1..74d9def9 100644 --- a/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs +++ b/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs @@ -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.26' }, + serverInfo: { name: 'minimax-bot', version: '0.7.28' }, instructions: t('mcp.instructions'), }; break; diff --git a/plugins/minimax-ai/minimax-bot/miniapp/node/store.mjs b/plugins/minimax-ai/minimax-bot/miniapp/node/store.mjs index 96093047..a2e2fec8 100644 --- a/plugins/minimax-ai/minimax-bot/miniapp/node/store.mjs +++ b/plugins/minimax-ai/minimax-bot/miniapp/node/store.mjs @@ -620,12 +620,18 @@ export class Store { async deleteChat(chatId) { await this.getMessages(chatId); + // Empty the transcript only after the state that forgets this chat is on disk. The messages are + // the one thing here that cannot be rebuilt: truncating them first means a failed save leaves + // the chat listed and the history already gone. In the opposite order a failed save leaves both + // intact, so the caller can simply try again. The in-memory entry is dropped only once the write + // succeeded, so a failure cannot leave memory ahead of the disk either. await this.enqueue(async () => { + const { [chatId]: forgotten, ...sessions } = this.state.sessions; + await writeJsonAtomic(this.statePath, { ...this.state, sessions }); + this.state.sessions = sessions; await writeJsonAtomic(this.messagePath(chatId), []); this.messageCache.set(chatId, []); - delete this.state.sessions[chatId]; }); - await this.save(); } /** Sidebar summary of one chat, read from the warm cache. */ diff --git a/plugins/minimax-ai/minimax-bot/package.json b/plugins/minimax-ai/minimax-bot/package.json index d81662b6..8035a030 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.26", + "version": "0.7.28", "private": true, "type": "module", "mcode": { diff --git a/plugins/minimax-ai/minimax-bot/tests/storage-reliability.test.mjs b/plugins/minimax-ai/minimax-bot/tests/storage-reliability.test.mjs index 7f53ac16..aef39bbb 100644 --- a/plugins/minimax-ai/minimax-bot/tests/storage-reliability.test.mjs +++ b/plugins/minimax-ai/minimax-bot/tests/storage-reliability.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -import { appendFile, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Store } from '../miniapp/node/store.mjs'; @@ -83,6 +83,24 @@ test('concurrent appends and reactions retain every update and assign unique ord assert.equal((await restored.getMessages(chat)).length, 1); }); +test('a reset that cannot be saved leaves the transcript on disk instead of emptying it', async (t) => { + const { dir, store, chat } = await fixture(t); + const kept = await store.appendMessage(chat, { role: 'user', text: 'Worth keeping' }); + await store.writeChain; + + // Put a directory where state.json belongs so the state write fails, and record what the + // transcript looked like before the reset was attempted. + const statePath = join(dir, 'state.json'); + await rm(statePath, { force: true }); + await mkdir(statePath, { recursive: true }); + + await assert.rejects(store.deleteChat(chat)); + // The chat is still listed because its state never reached disk, so its history has to survive + // too: emptying the transcript first is what made a failed reset destroy the conversation. + const onDisk = JSON.parse(await readFile(store.messagePath(chat), 'utf8')); + assert.equal(onDisk.filter((message) => message.id === kept.id).length, 1); +}); + test('legacy sequence migration preserves text/tool order and stable cursors across restarts', async (t) => { const { dir, store, chat } = await fixture(t); const parts = [{ type: 'text', text: 'The current conclusion: ' }, { type: 'tool', name: 'build', state: 'ok' }, { type: 'text', text: 'The current conclusion: publish after the build passes.' }];