diff --git a/bot.py b/bot.py index d8ee9de..9c902cf 100644 --- a/bot.py +++ b/bot.py @@ -81,12 +81,14 @@ class ApprovalView(discord.ui.View): def detect_project_name(conv_dir: Path) -> str: """Extract project name from conversation artifacts. - Output: lowercase_with_underscores (e.g. 'gravity_control') + Returns: lowercase_with_underscores (e.g. 'gravity_control') + Uses FIRST successful extraction and caches it. """ short_id = conv_dir.name[:8] def _sanitize(raw: str) -> str: - for suffix in ["Task Tracker", "— Task Tracker", "태스크", "구현 계획"]: + for suffix in ["Task Tracker", "— Task Tracker", "태스크", "구현 계획", + "Implementation Plan", "Walkthrough"]: raw = raw.replace(suffix, "") raw = raw.strip(" —-–") raw = re.sub(r'[^a-zA-Z0-9가-힣\s\-]', '', raw) @@ -102,7 +104,8 @@ def detect_project_name(conv_dir: Path) -> str: match = re.match(r'^#\s+(.+)', line) if match: name = _sanitize(match.group(1)) - if name and name != "task": + # Require at least 5 chars to avoid short generic names + if name and name != "task" and len(name) >= 5: return name except (OSError, UnicodeDecodeError): pass @@ -161,20 +164,40 @@ class GravityBot(commands.Bot): await self._reconnect_existing_channels() async def _reconnect_existing_channels(self): - """Scan existing Discord channels and map them to conversation IDs.""" + """Scan existing Discord channels and map them — MERGE same-name channels.""" if not self.session_category: return - count = 0 + # Group channels by normalized name + name_to_channel: dict[str, discord.TextChannel] = {} + duplicates: list[discord.TextChannel] = [] + for ch in self.session_category.text_channels: if ch.topic and "Antigravity Session:" in ch.topic: - conv_id = ch.topic.replace("Antigravity Session:", "").strip() - if conv_id: - self.session_channels[conv_id] = ch - await self._recover_task_message(ch, conv_id) - count += 1 + if ch.name in name_to_channel: + # DUPLICATE — mark for cleanup + duplicates.append(ch) + else: + name_to_channel[ch.name] = ch - logger.info(f"Reconnected to {count} existing channels") + # Map the primary channel for each name + count = 0 + for ch in name_to_channel.values(): + conv_id = ch.topic.replace("Antigravity Session:", "").strip() + if conv_id: + self.session_channels[conv_id] = ch + await self._recover_task_message(ch, conv_id) + count += 1 + + # Delete duplicate channels + for ch in duplicates: + try: + await ch.delete(reason="Duplicate channel cleanup") + logger.info(f"Deleted duplicate channel: #{ch.name}") + except (discord.Forbidden, discord.HTTPException) as e: + logger.warning(f"Failed to delete duplicate #{ch.name}: {e}") + + logger.info(f"Reconnected to {count} channels, cleaned {len(duplicates)} duplicates") async def _recover_task_message( self, channel: discord.TextChannel, conversation_id: str @@ -196,21 +219,25 @@ class GravityBot(commands.Bot): async def _ensure_channel( self, conversation_id: str, project_name: str ) -> discord.TextChannel: - """Get or create a channel. Uses GLOBAL lock + OWN dict lookup (no cache issues).""" - # Fast path: already mapped + """Get or create a channel. SINGLE channel per project name, guaranteed.""" + channel_name = f"{Config.CHANNEL_PREFIX}-{project_name}" + target_name = channel_name.lower().replace(" ", "-") + + # Fast path: this conv_id already mapped if conversation_id in self.session_channels: - return self.session_channels[conversation_id] + ch = self.session_channels[conversation_id] + # Verify the channel name matches (project name might have changed) + if ch.name == target_name: + return ch async with self._channel_create_lock: # Double-check after lock if conversation_id in self.session_channels: - return self.session_channels[conversation_id] + ch = self.session_channels[conversation_id] + if ch.name == target_name: + return ch - channel_name = f"{Config.CHANNEL_PREFIX}-{project_name}" - target_name = channel_name.lower().replace(" ", "-") - - # Check OWN dict for a channel with the same NAME - # (different conv IDs can share one channel) + # Check ALL mapped channels for same name for cid, ch in self.session_channels.items(): if ch.name == target_name: self.session_channels[conversation_id] = ch @@ -218,7 +245,7 @@ class GravityBot(commands.Bot): logger.info(f"Reusing channel #{ch.name} for {conversation_id[:8]}") return ch - # Create new channel (truly first time) + # Create new channel (truly no match anywhere) try: channel = await self.guild.create_text_channel( name=channel_name,