NousResearch/hermes-agent

Background process notifications routed to wrong session or thread

Cluster 9532 · gateway. Data as of

2 open items. Fix merged in #10460 on 15 Apr 2026. Check it covers your case.

The short answer

0open issues
2open PRs
11items grouped
Known?
Yes. 11 related items grouped here, 4 issues already closed.
Fix event
A fix was merged to main in PR #10460. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #10460. That is separate from the fix event.
Still open
0 issues and 2 PRs.

Most members concern background process or watch-pattern notifications being routed to the wrong session or thread (Telegram DM topics, Discord, Slack), with proposed changes to route them to the originating session. One member, "fix: resolve 7 identified issues [automated]", has a generic title and may not belong to this theme.

Open issues0

No open issues.

Open PRs2

  • PR#12918fix(gateway): route watch notifications to correct Discord thread (#10411)
  • PR#18337fix: resolve 7 identified issues [automated]

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #10460 fix(gateway): route watch-pattern notifications to originating session

    merged to main

    files: gateway/run.py, tests/gateway/test_background_process_notifications.py, tests/gateway/test_internal_event_bypass_pairing.py, tests/tools/test_watch_patterns.py, tools/process_registry.py, tools/terminal_tool.py

    Show the diff
    • gateway/run.py
      @@ -482,6 +482,27 @@ def _resolve_hermes_bin() -> Optional[list[str]]:
           return None
       
       
      +def _parse_session_key(session_key: str) -> "dict | None":
      +    """Parse a session key into its component parts.
      +
      +    Session keys follow the format
      +    ``agent:main:{platform}:{chat_type}:{chat_id}[:{thread_id}[:{user_id}]]``.
      +    Returns a dict with ``platform``, ``chat_type``, ``chat_id``, and
      +    optionally ``thread_id`` keys, or None if the key doesn't match.
      +    """
      +    parts = session_key.split(":")
      +    if len(parts) >= 5 and parts[0] == "agent" and parts[1] == "main":
      +        result = {
      +            "platform": parts[2],
      +            "chat_type": parts[3],
      +            "chat_id": parts[4],
      +        }
      +        if len(parts) > 5:
      +            result["thread_id"] = parts[5]
      +        return result
      +    return None
      +
      +
       def _format_gateway_process_notification(evt: dict) -> "str | None":
           """Format a watch pattern event from completion_queue into a [SYSTEM:] message."""
           evt_type = evt.get("type", "completion")
      @@ -1489,12 +1510,11 @@ async def _notify_active_sessions_of_shutdown(self) -> None:
               notified: set = set()
               for session_key in active:
                   # Parse platform + chat_id from the session key.
      -            # Format: agent:main:{platform}:{chat_type}:{chat_id}[:{extra}...]
      -            parts = session_key.split(":")
      -            if len(parts) < 5:
      +            _parsed = _parse_session_key(session_key)
      +            if not _parsed:
                       continue
      -            platform_str = parts[2]
      -            chat_id = parts[4]
      +            platform_str = _parsed["platform"]
      +            chat_id = _parsed["chat_id"]
       
                   # Deduplicate: one notification per chat, even if multiple
                   # sessions (different users/threads) share the same chat.
      @@ -1510,7 +1530,7 @@ async def _notify_active_sessions_of_shutdown(self) -> None:
       
                       # Include thread_id if present so the message lands in the
                       # correct forum topic / thread.
      -                thread_id = parts[5] if len(parts) > 5 else None
      +                thread_id = _parsed.get("thread_id")
                       metadata = {"thread_id": thread_id} if thread_id else None
       
                       await adapter.send(chat_id, msg, metadata=metadata)
      @@ -3958,7 +3978,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str):
                           synth_text = _format_gateway_process_notification(evt)
                           if synth_text:
                               try:
      -                            await self._inject_watch_notification(synth_text, event)
      +                            await self._inject_watch_notification(synth_text, evt)
                               except Exception as e2:
                                   logger.error("Watch notification injection error: %s", e2)
                   except Exception as e:
      @@ -7452,14 +7472,75 @@ async def _enrich_message_with_transcription(
                   return prefix
               return user_text
       
      -    async def _inject_watch_notification(self, synth_text: str, original_event) -> None:
      +    def _build_process_event_source(self, evt: dict):
      +        """Resolve the canonical source for a synthetic background-process event.
      +
      +        Prefer the persisted session-store origin for the event's session key.
      +        Falling back to the currently active foreground event is what causes
      +        cross-topic bleed, so don't do that.
      +        """
      +        from gateway.session import SessionSource
      +
      +        session_key = str(evt.get("session_key") or "").strip()
      +        derived_platform = ""
      +        derived_chat_type = ""
      +        derived_chat_id = ""
      +
      +        if session_key:
      +            try:
      +                self.session_store._ensure_loaded()
      +                entry = self.session_store._entries.get(session_key)
      +                if entry and getattr(entry, "origin", None):
      +                    return entry.origin
      +            except Exception as exc:
      +                logger.debug(
      +                    "Synthetic process-event session-store lookup failed for %s: %s",
      +                    session_key,
      +                    exc,
      +                )
      +
      +            _parsed = _parse_session_key(session_key)
      +            if _parsed:
      +                derived_platform = _parsed["platform"]
      +                derived_chat_type = _parsed["chat_type"]
      +                derived_chat_id = _parsed["chat_id"]
      +
      +        platform_name = str(evt.get("platform") or derived_platform or "").strip().lower()
      +        chat_type = str(evt.get("chat_type") or derived_chat_type or "").strip().lower()
      +        chat_id = str(evt.get("chat_id") or derived_chat_id or "").strip()
      +        if not platform_name or not chat_type or not chat_id:
      +            return None
      +
      +        try:
      +            platform = Platform(platform_name)
      +        except Exception:
      +            logger.warning(
      +                "Synthetic process event has invalid platform metadata: %r",
      +                platform_name,
      +            )
      +            return None
      +
      +        return SessionSource(
      +            platform=platform,
      +            chat_id=chat_id,
      +            chat_type=chat_type,
      +            thread_id=str(evt.get("thread_id") or "").strip() or None,
      +            user_id=str(evt.get("user_id") or "").strip() or None,
      +            user_name=str(evt.get("user_name") or "").strip() or None,
      +        )
      +
      +    async def _inject_watch_notification(self, synth_text: str, evt: dict) -> None:
               """Inject a watch-pattern notification as a synthetic message event.
       
      -        Uses the source from the original user event to route the notification
      -        back to the correct chat/adapter.
      +        Routing must come from the queued watch event itself, not from whatever
      +        foreground message happened to be active when the queue was drained.
               """
      -        source = getattr(original_event, "source", None)
      +        source = self._build_process_event_source(evt)
               if not source:
      +            logger.warning(
      +                "Dropping watch notification with no routing metadata for process %s",
      +                evt.get("session_id", "unknown"),
      +            )
                   return
               platform_name = source.platform.value if hasattr(source.platform, "value") else str(source.platform)
               adapter = None
      @@ -7477,7 +7558,12 @@ async def _inject_watch_notification(self, synth_text: str, original_event) -> N
                       source=source,
                       internal=True,
                   )
      -            logger.info("Watch pattern notification — injecting for %s", platform_name)
      +            logger.info(
      +                "Watch pattern notification — injecting for %s chat=%s thread=%s",
      +                platform_name,
      +                source.chat_id,
      +                source.thread_id,
      +            )
                   await adapter.handle_message(synth_event)
               except Exception as e:
                   logger.error("Watch notification injection error: %s", e)
      @@ -7547,33 +7633,42 @@ async def _run_process_watcher(self, watcher: dict) -> None:
                               f"Command: {session.command}\n"
                               f"Output:\n{_out}]"
                           )
      +                    source = self._build_process_event_source({
      +                        "session_id": session_id,
      +                        "session_key": session_key,
      +                        "platform": platform_name,
      +                        "chat_id": chat_id,
      +                        "thread_id": thread_id,
      +                        "user_id": user_id,
      +                        "user_name": user_name,
      +                    })
      +                    if not source:
      +                        logger.warning(
      +                            "Dropping completion notification with no routing metadata for process %s",
      +                            session_id,
      +                        )
      +                        break
      +
                           adapter = None
                           for p, a in self.adapters.items():
      -                        if p.value == platform_name:
      +                        if p == source.platform:
                                   adapter = a
                                   break
      -                    if adapter and chat_id:
      +                    if adapter and source.chat_id:
                               try:
                                   from gateway.platforms.base import MessageEvent, MessageType
      -                            from gateway.session import SessionSource
      -                            from gateway.config import Platform
      -                            _platform_enum = Platform(platform_name)
      -                            _source = SessionSource(
      -                                platform=_platform_enum,
      -                                chat_id=chat_id,
      -                                thread_id=thread_id or None,
      -                                user_id=user_id or None,
      -                                user_name=user_name or None,
      -                            )
                                   synth_event = MessageEvent(
                                       text=synth_text,
                                       message_type=MessageType.TEXT,
      -                                source=_source,
      +                                source=source,
                                       internal=True,
                                   )
                                   logger.info(
      -                                "Process %s finished — injecting agent notification for session %s",
      -                                session_id, session_key,
      +                                "Process %s finished — injecting agent notification for session %s chat=%s thread=%s",
      +                                session_id,
      +                                session_key,
      +                                source.chat_id,
      +                                source.thread_id,
                                   )
                                   await adapter.handle_message(synth_event)
                               except Exception as e:
    • tests/gateway/test_background_process_notifications.py
      @@ -14,7 +14,7 @@
       import pytest
       
       from gateway.config import GatewayConfig, Platform
      -from gateway.run import GatewayRunner
      +from gateway.run import GatewayRunner, _parse_session_key
       
       
       # ---------------------------------------------------------------------------
      @@ -45,7 +45,7 @@ def _build_runner(monkeypatch, tmp_path, mode: str) -> GatewayRunner:
           monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
       
           runner = GatewayRunner(GatewayConfig())
      -    adapter = SimpleNamespace(send=AsyncMock())
      +    adapter = SimpleNamespace(send=AsyncMock(), handle_message=AsyncMock())
           runner.adapters[Platform.TELEGRAM] = adapter
           return runner
       
      @@ -243,3 +243,162 @@ async def _instant_sleep(*_a, **_kw):
           assert adapter.send.await_count == 1
           _, kwargs = adapter.send.call_args
           assert kwargs["metadata"] is None
      +
      +
      [redacted:email]
      +async def test_inject_watch_notification_routes_from_session_store_origin(monkeypatch, tmp_path):
      +    from gateway.session import SessionSource
      +
      +    runner = _build_runner(monkeypatch, tmp_path, "all")
      +    adapter = runner.adapters[Platform.TELEGRAM]
      +    runner.session_store._entries["agent:main:telegram:group:-100:42"] = SimpleNamespace(
      +        origin=SessionSource(
      +            platform=Platform.TELEGRAM,
      +            chat_id="-100",
      +            chat_type="group",
      +            thread_id="42",
      +            user_id="123",
      +            user_name="Emiliyan",
      +        )
      +    )
      +
      +    evt = {
      +        "session_id": "proc_watch",
      +        "session_key": "agent:main:telegram:group:-100:42",
      +    }
      +
      +    await runner._inject_watch_notification("[SYSTEM: Background process matched]", evt)
      +
      +    adapter.handle_message.assert_awaited_once()
      +    synth_event = adapter.handle_message.await_args.args[0]
      +    assert synth_event.internal is True
      +    assert synth_event.source.platform == Platform.TELEGRAM
      +    assert synth_event.source.chat_id == "-100"
      +    assert synth_event.source.chat_type == "group"
      +    assert synth_event.source.thread_id == "42"
      +    assert synth_event.source.user_id == "123"
      +    assert synth_event.source.user_name == "Emiliyan"
      +
      +
      +def test_build_process_event_source_falls_back_to_session_key_chat_type(monkeypatch, tmp_path):
      +    runner = _build_runner(monkeypatch, tmp_path, "all")
      +
      +    evt = {
      +        "session_id": "proc_watch",
      +        "session_key": "agent:main:telegram:group:-100:42",
      +        "platform": "telegram",
      +        "chat_id": "-100",
      +        "thread_id": "42",
      +        "user_id": "123",
      +        "user_name": "Emiliyan",
      +    }
      +
      +    source = runner._build_process_event_source(evt)
      +
      +    assert source is not None
      +    assert source.platform == Platform.TELEGRAM
      +    assert source.chat_id == "-100"
      +    assert source.chat_type == "group"
      +    assert source.thread_id == "42"
      +    assert source.user_id == "123"
      +    assert source.user_name == "Emiliyan"
      +
      +
      [redacted:email]
      +async def test_inject_watch_notification_ignores_foreground_event_source(monkeypatch, tmp_path):
      +    """Negative test: watch notification must NOT route to the foreground thread."""
      +    from gateway.session import SessionSource
      +
      +    runner = _build_runner(monkeypatch, tmp_path, "all")
      +    adapter = runner.adapters[Platform.TELEGRAM]
      +
      +    # Session store has the process's original thread (thread 42)
      +    runner.session_store._entries["agent:main:telegram:group:-100:42"] = SimpleNamespace(
      +        origin=SessionSource(
      +            platform=Platform.TELEGRAM,
      +            chat_id="-100",
      +            chat_type="group",
      +            thread_id="42",
      +            user_id="proc_owner",
      +            user_name="alice",
      +        )
      +    )
      +
      +    # The evt dict carries the correct session_key — NOT a foreground event
      +    evt = {
      +        "session_id": "proc_cross_thread",
      +        "session_key": "agent:main:telegram:group:-100:42",
      +    }
      +
      +    await runner._inject_watch_notification("[SYSTEM: watch match]", evt)
      +
      +    adapter.handle_message.assert_awaited_once()
      +    synth_event = adapter.handle_message.await_args.args[0]
      +    # Must route to thread 42 (process origin), NOT some other thread
      +    assert synth_event.source.thread_id == "42"
      +    assert synth_event.source.user_id == "proc_owner"
      +
      +
      +def test_build_process_event_source_returns_none_for_empty_evt(monkeypatch, tmp_path):
      +    """Missing session_key and no platform metadata → None (drop notification)."""
      +    runner = _build_runner(monkeypatch, tmp_path, "all")
      +
      +    source = runner._build_process_event_source({"session_id": "proc_orphan"})
      +    assert source is None
      +
      +
      +def test_build_process_event_source_returns_none_for_invalid_platform(monkeypatch, tmp_path):
      +    """Invalid platform string → None."""
      +    runner = _build_runner(monkeypatch, tmp_path, "all")
      +
      +    evt = {
      +        "session_id": "proc_bad",
      +        "platform": "not_a_real_platform",
      +        "chat_type": "dm",
      +        "chat_id": "123",
      +    }
      +    source = runner._build_process_event_source(evt)
      +    assert source is None
      +
      +
      +def test_build_process_event_source_returns_none_for_short_session_key(monkeypatch, tmp_path):
      +    """Session key with <5 parts doesn't parse, falls through to empty metadata → None."""
      +    runner = _build_runner(monkeypatch, tmp_path, "all")
      +
      +    evt = {
      +        "session_id": "proc_short",
      +        "session_key": "agent:main:telegram",  # Too few parts
      +    }
      +    source = runner._build_process_event_source(evt)
      +    assert source is None
      +
      +
      +# ---------------------------------------------------------------------------
      +# _parse_session_key helper
      +# ---------------------------------------------------------------------------
      +
      +def test_parse_session_key_valid():
      +    result = _parse_session_key("agent:main:telegram:group:-100")
      +    assert result == {"platform": "telegram", "chat_type": "group", "chat_id": "-100"}
      +
      +
      +def test_parse_session_key_with_extra_parts():
      +    """Thread ID (6th part) is extracted; further parts are ignored."""
      +    result = _parse_session_key("agent:main:discord:group:chan123:thread456")
      +    assert result == {"platform": "discord", "chat_type": "group", "chat_id": "chan123", "thread_id": "thread456"}
      +
      +
      +def test_parse_session_key_with_user_id_part():
      +    """7th part (user_id) is ignored — only up to thread_id is extracted."""
      +    result = _parse_session_key("agent:main:telegram:group:chat1:thread42:user99")
      +    assert result == {"platform": "telegram", "chat_type": "group", "chat_id": "chat1", "thread_id": "thread42"}
      +
      +
      +def test_parse_session_key_too_short():
      +    assert _parse_session_key("agent:main:telegram") is None
      +    assert _parse_session_key("") is None
      +
      +
      +def test_parse_session_key_wrong_prefix():
      +    assert _parse_session_key("cron:main:telegram:dm:123") is None
      +    assert _parse_session_key("agent:cron:telegram:dm:123") is None
    • tests/gateway/test_internal_event_bypass_pairing.py
      @@ -230,6 +230,59 @@ async def _instant_sleep(*_a, **_kw):
           assert event.source.user_name == "alice"
       
       
      [redacted:email]
      +async def test_notify_on_complete_uses_session_store_origin_for_group_topic(monkeypatch, tmp_path):
      +    import tools.process_registry as pr_module
      +    from gateway.session import SessionSource
      +
      +    sessions = [
      +        SimpleNamespace(
      +            output_buffer="done\n", exited=True, exit_code=0, command="echo test"
      +        ),
      +    ]
      +    monkeypatch.setattr(pr_module, "process_registry", _FakeRegistry(sessions))
      +
      +    async def _instant_sleep(*_a, **_kw):
      +        pass
      +    monkeypatch.setattr(asyncio, "sleep", _instant_sleep)
      +
      +    runner = GatewayRunner(GatewayConfig())
      +    adapter = SimpleNamespace(send=AsyncMock(), handle_message=AsyncMock())
      +    runner.adapters[Platform.TELEGRAM] = adapter
      +    runner.session_store._entries["agent:main:telegram:group:-100:42"] = SimpleNamespace(
      +        origin=SessionSource(
      +            platform=Platform.TELEGRAM,
      +            chat_id="-100",
      +            chat_type="group",
      +            thread_id="42",
      +            user_id="user-42",
      +            user_name="alice",
      +        )
      +    )
      +
      +    watcher = {
      +        "session_id": "proc_test_internal",
      +        "check_interval": 0,
      +        "session_key": "agent:main:telegram:group:-100:42",
      +        "platform": "telegram",
      +        "chat_id": "-100",
      +        "thread_id": "42",
      +        "notify_on_complete": True,
      +    }
      +
      +    await runner._run_process_watcher(watcher)
      +
      +    assert adapter.handle_message.await_count == 1
      +    event = adapter.handle_message.await_args.args[0]
      +    assert event.internal is True
      +    assert event.source.platform == Platform.TELEGRAM
      +    assert event.source.chat_id == "-100"
      +    assert event.source.chat_type == "group"
      +    assert event.source.thread_id == "42"
      +    assert event.source.user_id == "user-42"
      +    assert event.source.user_name == "alice"
      +
      +
       @pytest.mark.asyncio
       async def test_none_user_id_skips_pairing(monkeypatch, tmp_path):
           """A non-internal event with user_id=None should be silently dropped."""
    • tests/tools/test_watch_patterns.py
      @@ -92,6 +92,25 @@ def test_basic_match(self, registry):
               assert "disk full" in evt["output"]
               assert evt["session_id"] == "proc_test_watch"
       
      +    def test_match_carries_session_key_and_watcher_routing_metadata(self, registry):
      +        session = _make_session(watch_patterns=["ERROR"])
      +        session.session_key = "agent:main:telegram:group:-100:42"
      +        session.watcher_platform = "telegram"
      +        session.watcher_chat_id = "-100"
      +        session.watcher_user_id = "u123"
      +        session.watcher_user_name = "alice"
      +        session.watcher_thread_id = "42"
      +
      +        registry._check_watch_patterns(session, "ERROR: disk full\n")
      +        evt = registry.completion_queue.get_nowait()
      +
      +        assert evt["session_key"] == "agent:main:telegram:group:-100:42"
      +        assert evt["platform"] == "telegram"
      +        assert evt["chat_id"] == "-100"
      +        assert evt["user_id"] == "u123"
      +        assert evt["user_name"] == "alice"
      +        assert evt["thread_id"] == "42"
      +
           def test_multiple_patterns(self, registry):
               """First matching pattern is reported."""
               session = _make_session(watch_patterns=["WARN", "ERROR"])
    • tools/process_registry.py
      @@ -191,9 +191,15 @@ def _check_watch_patterns(self, session: ProcessSession, new_text: str) -> None:
                           session._watch_disabled = True
                           self.completion_queue.put({
                               "session_id": session.id,
      +                        "session_key": session.session_key,
                               "command": session.command,
                               "type": "watch_disabled",
                               "suppressed": session._watch_suppressed,
      +                        "platform": session.watcher_platform,
      +                        "chat_id": session.watcher_chat_id,
      +                        "user_id": session.watcher_user_id,
      +                        "user_name": session.watcher_user_name,
      +                        "thread_id": session.watcher_thread_id,
                               "message": (
                                   f"Watch patterns disabled for process {session.id} — "
                                   f"too many matches ({session._watch_suppressed} suppressed). "
      @@ -219,11 +225,17 @@ def _check_watch_patterns(self, session: ProcessSession, new_text: str) -> None:
       
               self.completion_queue.put({
                   "session_id": session.id,
      +            "session_key": session.session_key,
                   "command": session.command,
                   "type": "watch_match",
                   "pattern": matched_pattern,
                   "output": output,
                   "suppressed": suppressed,
      +            "platform": session.watcher_platform,
      +            "chat_id": session.watcher_chat_id,
      +            "user_id": session.watcher_user_id,
      +            "user_name": session.watcher_user_name,
      +            "thread_id": session.watcher_thread_id,
               })
       
           @staticmethod
    • tools/terminal_tool.py
      @@ -1384,14 +1384,10 @@ def terminal_tool(
                       if pty_disabled_reason:
                           result_data["pty_note"] = pty_disabled_reason
       
      -                # Mark for agent notification on completion
      -                if notify_on_complete and background:
      -                    proc_session.notify_on_complete = True
      -                    result_data["notify_on_complete"] = True
      -
      -                    # In gateway mode, auto-register a fast watcher so the
      -                    # gateway can detect completion and trigger a new agent
      -                    # turn.  CLI mode uses the completion_queue directly.
      +                # Populate routing metadata on the session so that
      +                # watch-pattern and completion notifications can be
      +                # routed back to the correct chat/thread.
      +                if background and (notify_on_complete or watch_patterns):
                           from gateway.session_context import get_session_env as _gse
                           _gw_platform = _gse("HERMES_SESSION_PLATFORM", "")
                           if _gw_platform:
      @@ -1404,16 +1400,26 @@ def terminal_tool(
                               proc_session.watcher_user_id = _gw_user_id
                               proc_session.watcher_user_name = _gw_user_name
                               proc_session.watcher_thread_id = _gw_thread_id
      +
      +                # Mark for agent notification on completion
      +                if notify_on_complete and background:
      +                    proc_session.notify_on_complete = True
      +                    result_data["notify_on_complete"] = True
      +
      +                    # In gateway mode, auto-register a fast watcher so the
      +                    # gateway can detect completion and trigger a new agent
      +                    # turn.  CLI mode uses the completion_queue directly.
      +                    if proc_session.watcher_platform:
                               proc_session.watcher_interval = 5
                               process_registry.pending_watchers.append({
                                   "session_id": proc_session.id,
                                   "check_interval": 5,
                                   "session_key": session_key,
      -                            "platform": _gw_platform,
      -                            "chat_id": _gw_chat_id,
      -                            "user_id": _gw_user_id,
      -                            "user_name": _gw_user_name,
      -                            "thread_id": _gw_thread_id,
      +                            "platform": proc_session.watcher_platform,
      +                            "chat_id": proc_session.watcher_chat_id,
      +                            "user_id": proc_session.watcher_user_id,
      +                            "user_name": proc_session.watcher_user_name,
      +                            "thread_id": proc_session.watcher_thread_id,
                                   "notify_on_complete": True,
                               })
       

4 PRs closed without merging

From the close comments: merged via another change 3 · superseded by another PR 1

  • #9537 fix(gateway): route synthetic background events to their originating session · merged via another change → #10460, #10446
  • #10446 fix(gateway): route watch-pattern notifications to originating session · merged via another change → #10460, #9537
  • #26945 fix(telegram): preserve DM topic routing for synthetic background process events · superseded by another PR → #27212, #9532, #27937
  • #27212 fix(telegram): reply-anchor background-process notifications into DM topics · merged via another change → #26945
Closed issues · 4
  • Closed#9532[Bug]: background process watch notifications can leak into the wrong threaded session
  • Closed#9736Bug: Background process completion notifications can route to the wrong session
  • Closed#10411[Bug]: Background watch_match notifications can be injected into the wrong Discord thread
  • Closed#18101[Bug]: Background process updates can leak into the wrong Slack thread
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.15: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means 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 Lab

At a glance

Subsystem
gateway
Merged PRs
#10460
Closed issues
4
Attempts
4 PRs closed without merging
Fix link checked
15 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.