diff --git a/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json b/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json index bfc0594..e08811f 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 17ce0f5..fd2d8c5 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 884b4cc..74d9def 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 9609304..a2e2fec 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 d81662b..8035a03 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 7f53ac1..aef39bb 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.' }];