{"record":{"id":"5c2100b363df22ab","repo":"agentscope-ai/agentscope","slug":"bot-platform-bot-id-r-already-drives-channel-ho","errorCode":null,"errorMessage":"Bot {platform_bot_id!r} already drives channel {holder!r}.","messagePattern":"Bot (.+?) already drives channel (.+?)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/agentscope/app/storage/_sql/_storage.py","lineNumber":1376,"sourceCode":"                MySQL's ``ON DUPLICATE KEY UPDATE`` fires on *any*\n                unique-key conflict — the write would silently overwrite\n                the holder instead of failing, and the same call would\n                behave differently per dialect.\n\n                Neither this check nor `ChannelService.create`'s is\n                atomic with the write, so two creates racing for one bot\n                can both pass. The constraint then decides, and how it\n                surfaces depends on the dialect: Postgres and SQLite\n                raise `IntegrityError`, while on MySQL the duplicate-key\n                update lands on the holder's row and the read-back of\n                the new id then raises `NoResultFound`, rolling the\n                transaction back. Either way nothing is committed.\n        \"\"\"\n        holder = await self.get_channel_id_by_platform_bot_id(\n            platform_bot_id,\n        )\n        if holder is not None and holder != record.id:\n            raise ValueError(\n                f\"Bot {platform_bot_id!r} already drives channel \"\n                f\"{holder!r}.\",\n            )\n        await self._write_row(\n            ChannelRow,\n            record,\n            extra={\"platform_bot_id\": platform_bot_id},\n        )\n        return record.id\n\n    async def get_channel(self, channel_id: str) -> ChannelRecord | None:\n        \"\"\"Fetch a channel record by its global id.\n\n        Args:\n            channel_id (`str`):\n                The channel id.\n\n        Returns:","sourceCodeStart":1358,"sourceCodeEnd":1394,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/app/storage/_sql/_storage.py#L1358-L1394","documentation":"Raised by SQLStorage.upsert_channel when the platform_bot_id is already bound to a different channel. One platform bot may drive only one channel, so the mapping platform_bot_id -> channel must stay unique.","triggerScenarios":"Calling upsert_channel with a record whose platform_bot_id is already registered to another channel id (holder != record.id).","commonSituations":"Re-registering a bot token for a new channel without deleting the old channel record; reusing bot credentials across multiple channels; multi-tenant configs sharing the same platform bot.","solutions":["Delete or re-point the existing channel that currently holds this platform_bot_id before upserting","Use a distinct platform bot id per channel","Update the existing channel record in place (same id) rather than creating a new one"],"exampleFix":"// before\nawait storage.upsert_channel(ChannelRecord(id=new_id, platform_bot_id=\"bot-1\"))\n// after\nholder = await storage.get_channel_id_by_platform_bot_id(\"bot-1\")\nif holder is not None:\n    await storage.delete_channel(holder)\nawait storage.upsert_channel(ChannelRecord(platform_bot_id=\"bot-1\", ...))","handlingStrategy":"validation","validationCode":"holder = await storage.get_channel_id_by_platform_bot_id(rec.platform_bot_id)\\nif holder is not None and holder != rec.id:\\n    await storage.delete_channel(holder)","typeGuard":null,"tryCatchPattern":"try:\\n    await storage.upsert_channel(rec)\\nexcept ValueError as e:\\n    if \\\"already drives channel\\\" in str(e): rebind_bot(rec)\\n    else: raise","preventionTips":["One bot token per channel; track bindings centrally","Delete old channel records before rebinding bots"],"tags":["channel","bot","unique-constraint","upsert"],"backgroundTag":"duplicate-record-name","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}