NousResearch/hermes-agent
Voice state namespace collision fix across platforms
Cluster 12542. Data as of
Nothing open on GitHub. Fix merged in #12844 on 20 Apr 2026. Check it covers your case.
The short answer
0open issues
0open PRs
3items grouped
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #12844. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #12844. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and PRs address a bug where voice mode state in the gateway collides across different platforms when chat IDs happen to match. The fix namespaces voice state by platform to prevent cross-platform conflicts.
Open issues0
Open PRs0
Fixes on record
-
#12844 fix(gateway): namespace voice mode state by platform (#12542)
files:
gateway/run.py,tests/gateway/test_voice_command.py,tests/gateway/test_voice_mode_platform_isolation.pyShow the diff
-
gateway/run.py@@ -786,6 +786,10 @@ def _has_setup_skill(self) -> bool: _VOICE_MODE_PATH = _hermes_home / "gateway_voice_mode.json" + def _voice_key(self, platform: Platform, chat_id: str) -> str: + """Return a platform-namespaced key for voice mode state.""" + return f"{platform.value}:{chat_id}" + def _load_voice_modes(self) -> Dict[str, str]: try: data = json.loads(self._VOICE_MODE_PATH.read_text()) @@ -796,11 +800,21 @@ def _load_voice_modes(self) -> Dict[str, str]: return {} valid_modes = {"off", "voice_only", "all"} - return { - str(chat_id): mode - for chat_id, mode in data.items() - if mode in valid_modes - } + result = {} + for chat_id, mode in data.items(): + if mode not in valid_modes: + continue + key = str(chat_id) + # Skip legacy unprefixed keys (warn and skip) + if ":" not in key: + logger.warning( + "Skipping legacy unprefixed voice mode key %r during migration. " + "Re-enable voice mode on that chat to rebuild the prefixed key.", + key, + ) + continue + result[key] = mode + return result def _save_voice_modes(self) -> None: try: @@ -826,9 +840,14 @@ def _sync_voice_mode_state_to_adapter(self, adapter) -> None: disabled_chats = getattr(adapter, "_auto_tts_disabled_chats", None) if not isinstance(disabled_chats, set): return + platform = getattr(adapter, "platform", None) + if not isinstance(platform, Platform): + return disabled_chats.clear() + prefix = f"{platform.value}:" disabled_chats.update( - chat_id for chat_id, mode in self._voice_mode.items() if mode == "off" + key[len(prefix):] for key, mode in self._voice_mode.items() + if mode == "off" and key.startswith(prefix) ) async def _safe_adapter_disconnect(self, adapter, platform) -> None: @@ -5781,11 +5800,13 @@ async def _handle_voice_command(self, event: MessageEvent) -> str: """Handle /voice [on|off|tts|channel|leave|status] command.""" args = event.get_command_args().strip().lower() chat_id = event.source.chat_id + platform = event.source.platform + voice_key = self._voice_key(platform, chat_id) - adapter = self.adapters.get(event.source.platform) + adapter = self.adapters.get(platform) if args in ("on", "enable"): - self._voice_mode[chat_id] = "voice_only" + self._voice_mode[voice_key] = "voice_only" self._save_voice_modes() if adapter: self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=False) @@ -5795,13 +5816,13 @@ async def _handle_voice_command(self, event: MessageEvent) -> str: "Use /voice tts to get voice replies for all messages." ) elif args in ("off", "disable"): - self._voice_mode[chat_id] = "off" + self._voice_mode[voice_key] = "off" self._save_voice_modes() if adapter: self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=True) return "Voice mode disabled. Text-only replies." elif args == "tts": - self._voice_mode[chat_id] = "all" + self._voice_mode[voice_key] = "all" self._save_voice_modes() if adapter: self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=False) @@ -5814,7 +5835,7 @@ async def _handle_voice_command(self, event: MessageEvent) -> str: elif args == "leave": return await self._handle_voice_channel_leave(event) elif args == "status": - mode = self._voice_mode.get(chat_id, "off") + mode = self._voice_mode.get(voice_key, "off") labels = { "off": "Off (text only)", "voice_only": "On (voice reply to voice messages)", @@ -5838,15 +5859,15 @@ async def _handle_voice_command(self, event: MessageEvent) -> str: return f"Voice mode: {labels.get(mode, mode)}" else: # Toggle: off → on, on/all → off - current = self._voice_mode.get(chat_id, "off") + current = self._voice_mode.get(voice_key, "off") if current == "off": - self._voice_mode[chat_id] = "voice_only" + self._voice_mode[voice_key] = "voice_only" self._save_voice_modes() if adapter: self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=False) return "Voice mode enabled." else: - self._voice_mode[chat_id] = "off" + self._voice_mode[voice_key] = "off" self._save_voice_modes() if adapter: self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=True) @@ -5892,7 +5913,7 @@ async def _handle_voice_channel_join(self, event: MessageEvent) -> str: adapter._voice_text_channels[guild_id] = int(event.source.chat_id) if hasattr(adapter, "_voice_sources"): adapter._voice_sources[guild_id] = event.source.to_dict() - self._voice_mode[event.source.chat_id] = "all" + self._voice_mode[self._voice_key(event.source.platform, event.source.chat_id)] = "all" self._save_voice_modes() self._set_adapter_auto_tts_disabled(adapter, event.source.chat_id, disabled=False) return ( @@ -5919,7 +5940,7 @@ async def _handle_voice_channel_leave(self, event: MessageEvent) -> str: except Exception as e: logger.warning("Error leaving voice channel: %s", e) # Always clean up state even if leave raised an exception - self._voice_mode[event.source.chat_id] = "off" + self._voice_mode[self._voice_key(event.source.platform, event.source.chat_id)] = "off" self._save_voice_modes() self._set_adapter_auto_tts_disabled(adapter, event.source.chat_id, disabled=True) if hasattr(adapter, "_voice_input_callback"): @@ -5931,7 +5952,7 @@ def _handle_voice_timeout_cleanup(self, chat_id: str) -> None: Cleans up runner-side voice_mode state that the adapter cannot reach. """ - self._voice_mode[chat_id] = "off" + self._voice_mode[self._voice_key(Platform.DISCORD, chat_id)] = "off" self._save_voice_modes() adapter = self.adapters.get(Platform.DISCORD) self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=True) @@ -6017,7 +6038,7 @@ def _should_send_voice_reply( return False chat_id = event.source.chat_id - voice_mode = self._voice_mode.get(chat_id, "off") + voice_mode = self._voice_mode.get(self._voice_key(event.source.platform, chat_id), "off") is_voice_input = (event.message_type == MessageType.VOICE) should = ( -
tests/gateway/test_voice_command.py@@ -99,22 +99,22 @@ async def test_voice_on(self, runner): event = _make_event("/voice on") result = await runner._handle_voice_command(event) assert "enabled" in result.lower() - assert runner._voice_mode["123"] == "voice_only" + assert runner._voice_mode["telegram:123"] == "voice_only" @pytest.mark.asyncio async def test_voice_off(self, runner): - runner._voice_mode["123"] = "voice_only" + runner._voice_mode["telegram:123"] = "voice_only" event = _make_event("/voice off") result = await runner._handle_voice_command(event) assert "disabled" in result.lower() - assert runner._voice_mode["123"] == "off" + assert runner._voice_mode["telegram:123"] == "off" @pytest.mark.asyncio async def test_voice_tts(self, runner): event = _make_event("/voice tts") result = await runner._handle_voice_command(event) assert "tts" in result.lower() - assert runner._voice_mode["123"] == "all" + assert runner._voice_mode["telegram:123"] == "all" @pytest.mark.asyncio async def test_voice_status_off(self, runner): @@ -124,7 +124,7 @@ async def test_voice_status_off(self, runner): @pytest.mark.asyncio async def test_voice_status_on(self, runner): - runner._voice_mode["123"] = "voice_only" + runner._voice_mode["telegram:123"] = "voice_only" event = _make_event("/voice status") result = await runner._handle_voice_command(event) assert "voice reply" in result.lower() @@ -134,15 +134,15 @@ async def test_toggle_off_to_on(self, runner): event = _make_event("/voice") result = await runner._handle_voice_command(event) assert "enabled" in result.lower() - assert runner._voice_mode["123"] == "voice_only" + assert runner._voice_mode["telegram:123"] == "voice_only" @pytest.mark.asyncio async def test_toggle_on_to_off(self, runner): - runner._voice_mode["123"] = "voice_only" + runner._voice_mode["telegram:123"] = "voice_only" event = _make_event("/voice") result = await runner._handle_voice_command(event) assert "disabled" in result.lower() - assert runner._voice_mode["123"] == "off" + assert runner._voice_mode["telegram:123"] == "off" @pytest.mark.asyncio async def test_persistence_saved(self, runner): @@ -150,39 +150,47 @@ async def test_persistence_saved(self, runner): await runner._handle_voice_command(event) assert runner._VOICE_MODE_PATH.exists() data = json.loads(runner._VOICE_MODE_PATH.read_text()) - assert data["123"] == "voice_only" + assert data["telegram:123"] == "voice_only" @pytest.mark.asyncio async def test_persistence_loaded(self, runner): - runner._VOICE_MODE_PATH.write_text(json.dumps({"456": "all"})) + runner._VOICE_MODE_PATH.write_text(json.dumps({"telegram:456": "all"})) loaded = runner._load_voice_modes() - assert loaded == {"456": "all"} + assert loaded == {"telegram:456": "all"} @pytest.mark.asyncio async def test_persistence_saved_for_off(self, runner): event = _make_event("/voice off") await runner._handle_voice_command(event) data = json.loads(runner._VOICE_MODE_PATH.read_text()) - assert data["123"] == "off" + assert data["telegram:123"] == "off" def test_sync_voice_mode_state_to_adapter_restores_off_chats(self, runner): - runner._voice_mode = {"123": "off", "456": "all"} - adapter = SimpleNamespace(_auto_tts_disabled_chats=set()) + from gateway.config import Platform + runner._voice_mode = {"telegram:123": "off", "telegram:456": "all"} + adapter = SimpleNamespace( + _auto_tts_disabled_chats=set(), + platform=Platform.TELEGRAM, + ) runner._sync_voice_mode_state_to_adapter(adapter) assert adapter._auto_tts_disabled_chats == {"123"} def test_restart_restores_voice_off_state(self, runner, tmp_path): - runner._VOICE_MODE_PATH.write_text(json.dumps({"123": "off"})) + from gateway.config import Platform + runner._VOICE_MODE_PATH.write_text(json.dumps({"telegram:123": "off"})) restored_runner = _make_runner(tmp_path) restored_runner._voice_mode = restored_runner._load_voice_modes() - adapter = SimpleNamespace(_auto_tts_disabled_chats=set()) + adapter = SimpleNamespace( + _auto_tts_disabled_chats=set(), + platform=Platform.TELEGRAM, + ) restored_runner._sync_voice_mode_state_to_adapter(adapter) - assert restored_runner._voice_mode["123"] == "off" + assert restored_runner._voice_mode["telegram:123"] == "off" assert adapter._auto_tts_disabled_chats == {"123"} @pytest.mark.asyncio @@ -191,8 +199,21 @@ async def test_per_chat_isolation(self, runner): e2 = _make_event("/voice tts", chat_id="bbb") await runner._handle_voice_command(e1) await runner._handle_voice_command(e2) - assert runner._voice_mode["aaa"] == "voice_only" - assert runner._voice_mode["bbb"] == "all" + assert runner._voice_mode["telegram:aaa"] == "voice_only" + assert runner._voice_mode["telegram:bbb"] == "all" + + @pytest.mark.asyncio + async def test_platform_isolation(self, runner): + """Same chat_id on different platforms must not collide (#12542).""" + telegram_event = _make_event("/voice on", chat_id="999") + slack_event = _make_event("/voice off", chat_id="999") + slack_event.source.platform.value = "slack" + + await runner._handle_voice_command(telegram_event) + await runner._handle_voice_command(slack_event) + + assert runner._voice_mode["telegram:999"] == "voice_only" + assert runner._voice_mode["slack:999"] == "off" # ===================================================================== @@ -223,9 +244,9 @@ def _call(self, runner, voice_mode, message_type, agent_messages=None, """Call real _should_send_voice_reply on a GatewayRunner instance.""" chat_id = "123" if voice_mode != "off": - runner._voice_mode[chat_id] = voice_mode + runner._voice_mode["telegram:" + chat_id] = voice_mode else: - runner._voice_mode.pop(chat_id, None) + runner._voice_mode.pop("telegram:" + chat_id, None) event = _make_event(message_type=message_type) @@ -713,7 +734,7 @@ async def test_join_success(self, runner): result = await runner._handle_voice_channel_join(event) assert "joined" in result.lower() assert "General" in result - assert runner._voice_mode["123"] == "all" + assert runner._voice_mode["discord:123"] == "all" assert mock_adapter._voice_sources[111]["chat_id"] == "123" assert mock_adapter._voice_sources[111]["chat_type"] == "group" @@ -791,10 +812,10 @@ async def test_leave_success(self, runner): mock_adapter.leave_voice_channel = AsyncMock() event = self._make_discord_event("/voice leave") runner.adapters[event.source.platform] = mock_adapter - runner._voice_mode["123"] = "all" + runner._voice_mode["discord:123"] = "all" result = await runner._handle_voice_channel_leave(event) assert "left" in result.lower() - assert runner._voice_mode["123"] == "off" + assert runner._voice_mode["discord:123"] == "off" mock_adapter.leave_voice_channel.assert_called_once_with(111) # -- _handle_voice_channel_input -- @@ -1298,11 +1319,11 @@ async def test_leave_exception_still_cleans_state(self, runner): event = _make_event("/voice leave") event.raw_message = SimpleNamespace(guild_id=111, guild=None) runner.adapters[event.source.platform] = mock_adapter - runner._voice_mode["123"] = "all" + runner._voice_mode["telegram:123"] = "all" result = await runner._handle_voice_channel_leave(event) assert "left" in result.lower() - assert runner._voice_mode["123"] == "off" + assert runner._voice_mode["telegram:123"] == "off" assert mock_adapter._voice_input_callback is None @pytest.mark.asyncio @@ -1316,7 +1337,7 @@ async def test_leave_clears_callback(self, runner): event = _make_event("/voice leave") event.raw_message = SimpleNamespace(guild_id=111, guild=None) runner.adapters[event.source.platform] = mock_adapter - runner._voice_mode["123"] = "all" + runner._voice_mode["telegram:123"] = "all" await runner._handle_voice_channel_leave(event) assert mock_adapter._voice_input_callback is None @@ -1763,11 +1784,11 @@ async def test_timeout_calls_disconnect_callback(self, adapter): async def test_runner_cleanup_method_removes_voice_mode(self, tmp_path): """_handle_voice_timeout_cleanup removes voice_mode for chat.""" runner = _make_runner(tmp_path) - runner._voice_mode["999"] = "all" + runner._voice_mode["discord:999"] = "all" runner._handle_voice_timeout_cleanup("999") - assert runner._voice_mode["999"] == "off", \ + assert runner._voice_mode["discord:999"] == "off", \ "voice_mode must persist explicit off state after timeout cleanup" @pytest.mark.asyncio @@ -2524,7 +2545,7 @@ def _call_should_reply(self, runner, voice_mode, msg_type, response="Hello", agent_msgs=None, already_sent=False): from gateway.platforms.base import MessageType, MessageEvent, SessionSource from gateway.config import Platform - runner._voice_mode["ch1"] = voice_mode + runner._voice_mode["discord:ch1"] = voice_mode source = SessionSource( platform=Platform.DISCORD, chat_id="ch1", user_id="1", user_name="test", chat_type="channel", -
tests/gateway/test_voice_mode_platform_isolation.py@@ -0,0 +1,218 @@ +"""Tests for voice mode platform isolation (bug #12542). + +Voice mode state stored as {chat_id: mode} without a platform namespace +caused collisions: Telegram chat '123' and Slack chat '123' shared the +same key. The fix prefixes keys with platform value: 'telegram:123' vs +'slack:123'. +""" + +import json +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from gateway.config import Platform +from gateway.run import GatewayRunner + + +class TestVoiceKeyHelper: + """Test the _voice_key helper method.""" + + def test_voice_key_format(self): + """_voice_key returns 'platform:chat_id' format.""" + runner = _make_runner() + assert runner._voice_key(Platform.TELEGRAM, "123") == "telegram:123" + assert runner._voice_key(Platform.SLACK, "456") == "slack:456" + assert runner._voice_key(Platform.DISCORD, "789") == "discord:789" + + def test_voice_key_different_platforms_same_chat_id(self): + """Same chat_id on different platforms yields different keys.""" + runner = _make_runner() + key_telegram = runner._voice_key(Platform.TELEGRAM, "123") + key_slack = runner._voice_key(Platform.SLACK, "123") + key_discord = runner._voice_key(Platform.DISCORD, "123") + assert key_telegram != key_slack + assert key_slack != key_discord + assert key_telegram == "telegram:123" + assert key_slack == "slack:123" + assert key_discord == "discord:123" + + +class TestVoiceModePlatformIsolation: + """Test that voice mode state is isolated by platform.""" + + def test_telegram_and_slack_voice_mode_independent(self): + """Setting voice mode for Telegram chat '123' does not affect Slack chat '123'.""" + runner = _make_runner() + + # Enable voice mode for Telegram chat '123' + runner._voice_mode[runner._voice_key(Platform.TELEGRAM, "123")] = "all" + # Enable voice mode for Slack chat '123' to a different mode + runner._voice_mode[runner._voice_key(Platform.SLACK, "123")] = "voice_only" + + # Verify they are independent + assert runner._voice_mode.get(runner._voice_key(Platform.TELEGRAM, "123")) == "all" + assert runner._voice_mode.get(runner._voice_key(Platform.SLACK, "123")) == "voice_only" + + # Disabling Telegram should not affect Slack + runner._voice_mode[runner._voice_key(Platform.TELEGRAM, "123")] = "off" + assert runner._voice_mode.get(runner._voice_key(Platform.TELEGRAM, "123")) == "off" + assert runner._voice_mode.get(runner._voice_key(Platform.SLACK, "123")) == "voice_only" + + +class TestLegacyKeyMigration: + """Test migration of legacy unprefixed keys in _load_voice_modes.""" + + def test_load_voice_modes_skips_legacy_keys(self): + """_load_voice_modes skips keys without ':' prefix and logs a warning.""" + runner = _make_runner() + + # Simulate legacy persisted data with unprefixed keys + legacy_data = { + "123": "all", + "456": "voice_only", + # Also includes a properly prefixed key (from after the fix) + "telegram:789": "off", + } + + with tempfile.TemporaryDirectory() as tmpdir: + voice_path = Path(tmpdir) / "gateway_voice_mode.json" + voice_path.write_text(json.dumps(legacy_data)) + + with patch.object(runner, "_VOICE_MODE_PATH", voice_path): + with patch("gateway.run.logger") as mock_logger: + result = runner._load_voice_modes() + + # Legacy keys without ':' should be skipped + assert "123" not in result + assert "456" not in result + # Prefixed key should be preserved + assert result.get("telegram:789") == "off" + # Warning should be logged for each legacy key + assert mock_logger.warning.called + warning_calls = [str(call) for call in mock_logger.warning.call_args_list] + assert any("Skipping legacy unprefixed voice mode key" in str(c) for c in warning_calls) + + def test_load_voice_modes_preserves_prefixed_keys(self): + """_load_voice_modes correctly loads platform-prefixed keys.""" + runner = _make_runner() + + persisted_data = { + "telegram:123": "all", + "slack:456": "voice_only", + "discord:789": "off", + } + + with tempfile.TemporaryDirectory() as tmpdir: + voice_path = Path(tmpdir) / "gateway_voice_mode.json" + voice_path.write_text(json.dumps(persisted_data)) + + with patch.object(runner, "_VOICE_MODE_PATH", voice_path): + result = runner._load_voice_modes() + + assert result.get("telegram:123") == "all" + assert result.get("slack:456") == "voice_only" + assert result.get("discord:789") == "off" + + def test_load_voice_modes_invalid_modes_filtered(self): + """_load_voice_modes filters out invalid mode values.""" + runner = _make_runner() + + data = { + "telegram:123": "all", + "telegram:456": "invalid_mode", + "telegram:789": "voice_only", + } + + with tempfile.TemporaryDirectory() as tmpdir: + voice_path = Path(tmpdir) / "gateway_voice_mode.json" + voice_path.write_text(json.dumps(data)) + + with patch.object(runner, "_VOICE_MODE_PATH", voice_path): + result = runner._load_voice_modes() + + assert result.get("telegram:123") == "all" + assert "telegram:456" not in result + assert result.get("telegram:789") == "voice_only" + + +class TestSyncVoiceModeStateToAdapter: + """Test _sync_voice_mode_state_to_adapter filters by platform.""" + + def test_sync_only_includes_platform_chats(self): + """Only chats matching the adapter's platform are synced.""" + runner = _make_runner() + + # Set up voice mode state with multiple platforms + runner._voice_mode = { + "telegram:123": "off", # Should sync + "telegram:456": "all", # Should NOT sync (mode is not "off") + "slack:123": "off", # Should NOT sync (different platform) + "discord:789": "off", # Should NOT sync (different platform) + } + + # Create a mock Telegram adapter + mock_adapter = MagicMock() + mock_adapter.platform = Platform.TELEGRAM + mock_adapter._auto_tts_disabled_chats = set() + + runner._sync_voice_mode_state_to_adapter(mock_adapter) + + # Only telegram:123 should be in disabled_chats (mode="off" for telegram) + assert mock_adapter._auto_tts_disabled_chats == {"123"} + + def test_sync_clears_existing_state(self): + """_sync_voice_mode_state_to_adapter clears existing disabled_chats first.""" + runner = _make_runner() + + runner._voice_mode = { + "telegram:123": "off", + } + + mock_adapter = MagicMock() + mock_adapter.platform = Platform.TELEGRAM + mock_adapter._auto_tts_disabled_chats = {"old_chat_id", "another_old"} + + runner._sync_voice_mode_state_to_adapter(mock_adapter) + + # Old entries should be cleared + assert mock_adapter._auto_tts_disabled_chats == {"123"} + + def test_sync_returns_early_without_platform(self): + """_sync_voice_mode_state_to_adapter returns early if adapter has no platform.""" + runner = _make_runner() + runner._voice_mode = {"telegram:123": "off"} + + mock_adapter = MagicMock() + mock_adapter.platform = None + mock_adapter._auto_tts_disabled_chats = {"old"} + + runner._sync_voice_mode_state_to_adapter(mock_adapter) + + # disabled_chats should not be modified + assert mock_adapter._auto_tts_disabled_chats == {"old"} + + def test_sync_returns_early_without_auto_tts_disabled_chats(self): + """_sync_voice_mode_state_to_adapter returns early if adapter lacks _auto_tts_disabled_chats.""" + runner = _make_runner() + runner._voice_mode = {"telegram:123": "off"} + + mock_adapter = MagicMock(spec=[]) # No _auto_tts_disabled_chats attribute + + # Should not raise + runner._sync_voice_mode_state_to_adapter(mock_adapter) + + +# --------------------------------------------------------------------------- +# Helper +# --------------------------------------------------------------------------- + +def _make_runner() -> GatewayRunner: + """Create a minimal GatewayRunner for testing.""" + with patch("gateway.run.GatewayRunner._load_voice_modes", return_value={}): + runner = GatewayRunner.__new__(GatewayRunner) + runner._voice_mode = {} + runner.adapters = {} + return runner
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#12824 fix(gateway): namespace voice mode state by platform to prevent cross-platform collision · merged via another change →
#12844
Closed issues · 1
- Closed#12542gateway: /voice state collides across platforms when chat IDs match
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #12844
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 20 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.