{"repo":"NousResearch/hermes-agent","public_id":"30170","display_label":"Background subagents interrupted or lost on new user message","summary":"A cluster of bugs and fixes around running background/delegate subagents being killed, detached, or disappearing from the UI when the user sends a new message mid-turn. Fixes address interrupt propagation, session clearing, and UI preservation of live subagents.","subsystem":"agent","counts":{"members":12,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":5,"merged_prs":3,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":32076},"resolution":{"state":"partially_resolved","by":"pr-32076,pr-65040,pr-72409"},"freshness":{"state":"verified","last_verified_at":"2026-07-28T21:20:11+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.1593939393939394},"fix_edges":[{"issue_number":30170,"pr_number":32076,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":64015,"pr_number":72409,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":65000,"pr_number":65040,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":67980,"pr_number":72409,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":30170,"title":"[Bug]: Sending a message while delegate_task is running kills the subagent — interrupt propagates unconditionally to children","url":null,"state":"closed"},{"kind":"issue","number":64015,"title":"[Bug]: Desktop: still-RUNNING background subagents vanish from the UI when the user sends a new message","url":null,"state":"closed"},{"kind":"issue","number":65000,"title":"Mid-turn user message interrupts/detaches running delegation subagent","url":null,"state":"closed"},{"kind":"issue","number":65001,"title":"Mid-turn user message interrupts/detaches running delegation subagent","url":null,"state":"closed"},{"kind":"issue","number":67980,"title":"[Bug]: Background subagents (async delegation) disappear from Desktop UI when user sends a new message — clearSessionSubagents() on message.start silently drops running subagents","url":null,"state":"closed"},{"kind":"pr","number":30183,"title":"fix(#30170): protect in-flight subagents from busy-mode interrupts","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#32076","number":32076}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":30819,"title":"fix(gateway): queue follow-ups while delegate subagents run","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#32076","number":32076},{"kind":"pr","value":"#30183","number":30183}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":32076,"title":"fix(gateway): protect in-flight subagents from busy-mode interrupts","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","tests/gateway/test_subagent_protection_30170.py"],"fix_hunks":[{"file":"gateway/run.py","hunks":["@@ -3034,6 +3034,44 @@ def _snapshot_running_agents(self) -> Dict[str, Any]:\n             if agent is not _AGENT_PENDING_SENTINEL\n         }\n \n+    @staticmethod\n+    def _agent_has_active_subagents(running_agent: Any) -> bool:\n+        \"\"\"Return True when *running_agent* is currently driving subagents\n+        via the ``delegate_task`` tool.\n+\n+        Background (#30170): ``AIAgent.interrupt()`` cascades through the\n+        parent's ``_active_children`` list and calls ``interrupt()`` on\n+        every child synchronously, which aborts in-flight subagent work\n+        and produces a fallback cascade with no actionable signal.\n+        Demoting ``busy_input_mode='interrupt'`` to ``queue`` semantics\n+        whenever this helper returns True protects subagent work from\n+        conversational follow-ups while leaving the explicit ``/stop``\n+        path (which goes through ``_interrupt_and_clear_session``)\n+        untouched. Safe-by-default: returns False on any attribute or\n+        lock error so a missing/broken parent never blocks the existing\n+        interrupt path.\n+        \"\"\"\n+        if running_agent is None or running_agent is _AGENT_PENDING_SENTINEL:\n+            return False\n+        children = getattr(running_agent, \"_active_children\", None)\n+        # AIAgent always initialises this as a concrete list (see\n+        # agent/agent_init.py). Reject anything that isn't a real\n+        # collection — this guards against ``MagicMock()._active_children``\n+        # auto-creating a truthy stub in tests and triggering the demotion\n+        # against an agent that doesn't actually have subagents.\n+        if not isinstance(children, (list, tuple, set)):\n+            return False\n+        if not children:\n+            return False\n+        lock = getattr(running_agent, \"_active_children_lock\", None)\n+        try:\n+            if lock is not None:\n+                with lock:\n+                    return bool(children)\n+            return bool(children)\n+        except Exception:\n+            return False\n+\n     def _queue_or_replace_pending_event(self, session_key: str, event: MessageEvent) -> None:\n         adapter = self.adapters.get(event.source.platform)\n         if not adapter:","@@ -3105,6 +3143,25 @@ async def _handle_active_session_busy_message(self, event: MessageEvent, session\n         # queueing + interrupting.  If the agent isn't running yet\n         # (sentinel) or lacks steer(), or the payload is empty, fall back\n         # to queue semantics so nothing is lost.\n+        # #30170 — Subagent protection. ``AIAgent.interrupt()`` cascades\n+        # to every entry in the parent's ``_active_children`` list and\n+        # aborts in-flight ``delegate_task`` work. Demote ``interrupt``\n+        # to ``queue`` when the parent is currently driving subagents so\n+        # a conversational follow-up doesn't destroy minutes of subagent\n+        # work. Explicit ``/stop`` and ``/new`` slash commands go through\n+        # ``_interrupt_and_clear_session`` and are unaffected — the\n+        # operator still has a way to force-cancel everything.\n+        demoted_for_subagents = (\n+            effective_mode == \"interrupt\"\n+            and self._agent_has_active_subagents(running_agent)\n+        )\n+        if demoted_for_subagents:\n+            logger.info(\n+                \"Demoting busy_input_mode 'interrupt' to 'queue' for session %s \"\n+                \"because the running agent has active subagents (#30170)\",\n+                session_key,\n+            )\n+            effective_mode = \"queue\"\n         steered = False\n         if effective_mode == \"steer\":\n             steer_text = (event.text or \"\").strip()","@@ -3192,6 +3249,14 @@ async def _handle_active_session_busy_message(self, event: MessageEvent, session\n                 f\"⏩ Steered into current run{status_detail}. \"\n                 f\"Your message arrives after the next tool call.\"\n             )\n+        elif is_queue_mode and demoted_for_subagents:\n+            # #30170 — explain the demotion so the user knows their\n+            # follow-up didn't accidentally kill the subagent and\n+            # discovers `/stop` as the explicit escape hatch.\n+            message = (\n+                f\"⏳ Subagent working{status_detail} — your message is queued for \"\n+                f\"when it finishes (use /stop to cancel everything).\"\n+            )\n         elif is_queue_mode:\n             message = (\n                 f\"⏳ Queued for the next turn{status_detail}. \"","@@ -7246,6 +7311,22 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:\n                 logger.debug(\"PRIORITY steer-fallback-to-queue for session %s\", _quick_key)\n                 self._queue_or_replace_pending_event(_quick_key, event)\n                 return None\n+            # #30170 — Subagent protection (PRIORITY path). Same rationale\n+            # as ``_handle_active_session_busy_message``: an interrupt\n+            # cascades through ``_active_children`` and aborts in-flight\n+            # delegate_task work. Demote to queue semantics when the\n+            # parent is currently driving subagents so a conversational\n+            # follow-up doesn't destroy minutes of subagent progress.\n+            # /stop reaches its dedicated handler above, so the operator\n+            # still has a clean escape hatch.\n+            if self._agent_has_active_subagents(running_agent):\n+                logger.info(\n+                    \"PRIORITY interrupt demoted to queue for session %s \"\n+                    \"because the running agent has active subagents (#30170)\",\n+                    _quick_key,\n+                )\n+                self._queue_or_replace_pending_event(_quick_key, event)\n+                return None\n             logger.debug(\"PRIORITY interrupt for session %s\", _quick_key)\n             running_agent.interrupt(event.text)\n             # NOTE: self._pending_messages was write-only (never consumed)."]},{"file":"tests/gateway/test_subagent_protection_30170.py","hunks":["@@ -0,0 +1,348 @@\n+\"\"\"Regression tests for #30170.\n+\n+#30170: Sending a message while ``delegate_task`` is running killed the\n+subagent because the gateway always called ``running_agent.interrupt()``\n+on the parent, which then cascaded synchronously through\n+``AIAgent._active_children`` and aborted every in-flight subagent. The\n+reporter (and the linked Phase-1 spec) asked for the gateway to demote\n+``busy_input_mode='interrupt'`` to ``queue`` semantics whenever the\n+parent is currently driving subagents, while leaving explicit ``/stop``\n+and ``/new`` slash commands untouched.\n+\n+These tests pin down the gateway-side guard introduced for #30170:\n+\n+* ``GatewayRunner._agent_has_active_subagents`` correctly recognises\n+  parents that own real children, without false-positives from a\n+  ``MagicMock()._active_children`` auto-attribute, missing locks, or\n+  the ``_AGENT_PENDING_SENTINEL`` placeholder.\n+* ``_handle_active_session_busy_message`` demotes the interrupt mode to\n+  queue semantics (no ``interrupt()`` call, message merged into the\n+  pending queue, ack reflects the demotion) when the parent has active\n+  subagents.\n+* The ``queue`` and ``steer`` configured modes still behave exactly as\n+  before — the guard is interrupt-only.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import sys\n+import threading\n+import time\n+import types\n+from typing import Any\n+from unittest.mock import AsyncMock, MagicMock, patch\n+\n+import pytest\n+\n+# ──────────────────────────────────────────────────────────────────────\n+# Minimal stubs so gateway imports cleanly (mirrors test_busy_session_ack)\n+# ──────────────────────────────────────────────────────────────────────\n+_tg = types.ModuleType(\"telegram\")\n+_tg.constants = types.ModuleType(\"telegram.constants\")\n+_ct = MagicMock()\n+_ct.SUPERGROUP = \"supergroup\"\n+_ct.GROUP = \"group\"\n+_ct.PRIVATE = \"private\"\n+_tg.constants.ChatType = _ct\n+sys.modules.setdefault(\"telegram\", _tg)\n+sys.modules.setdefault(\"telegram.constants\", _tg.constants)\n+sys.modules.setdefault(\"telegram.ext\", types.ModuleType(\"telegram.ext\"))\n+\n+from gateway.platforms.base import (  # noqa: E402\n+    MessageEvent,\n+    MessageType,\n+    SessionSource,\n+    build_session_key,\n+)\n+from gateway.run import GatewayRunner, _AGENT_PENDING_SENTINEL  # noqa: E402\n+\n+\n+# ──────────────────────────────────────────────────────────────────────\n+# Builders (parallel to tests/gateway/test_busy_session_ack.py)\n+# ──────────────────────────────────────────────────────────────────────\n+def _make_event(text: str = \"hello\", chat_id: str = \"123\") -> MessageEvent:\n+    source = SessionSource(\n+        platform=MagicMock(value=\"telegram\"),\n+        chat_id=chat_id,\n+        chat_type=\"private\",\n+        user_id=\"user1\",\n+    )\n+    return MessageEvent(\n+        text=text,\n+        message_type=MessageType.TEXT,\n+        source=source,\n+        message_id=\"msg1\",\n+    )\n+\n+\n+def _make_runner() -> GatewayRunner:\n+    runner = object.__new__(GatewayRunner)\n+    runner._running_agents = {}\n+    runner._running_agents_ts = {}\n+    runner._pending_messages = {}\n+    runner._busy_ack_ts = {}\n+    runner._draining = False\n+    runner.adapters = {}\n+    runner.config = MagicMock()\n+    runner.session_store = None\n+    runner.hooks = MagicMock()\n+    runner.hooks.emit = AsyncMock()\n+    runner.pairing_store = MagicMock()\n+    runner.pairing_store.is_approved.return_value = True\n+    runner._is_user_authorized = lambda _source: True\n+    return runner\n+\n+\n+def _make_adapter() -> MagicMock:\n+    adapter = MagicMock()\n+    adapter._pending_messages = {}\n+    adapter._send_with_retry = AsyncMock()\n+    adapter.config = MagicMock()\n+    adapter.config.extra = {}\n+    adapter.platform = MagicMock(value=\"telegram\")\n+    return adapter\n+\n+\n+def _make_parent_with_subagents(\n+    *, children: int = 1, with_lock: bool = True\n+) -> MagicMock:\n+    \"\"\"A MagicMock shaped like an AIAgent that currently owns *children* subagents.\"\"\"\n+    parent = MagicMock()\n+    parent._active_children = [MagicMock() for _ in range(children)]\n+    parent._active_children_lock = threading.Lock() if with_lock else None\n+    parent.get_activity_summary.return_value = {\n+        \"api_call_count\": 7,\n+        \"max_iterations\": 60,\n+        \"current_tool\": \"delegate_task\",\n+    }\n+    return parent\n+\n+\n+def _make_parent_no_subagents() -> MagicMock:\n+    \"\"\"A MagicMock shaped like an AIAgent that is NOT delegating.\"\"\"\n+    parent = MagicMock()\n+    parent._active_children = []\n+    parent._active_children_lock = threading.Lock()\n+    parent.get_activity_summary.return_value = {\n+        \"api_call_count\": 3,\n+        \"max_iterations\": 60,\n+        \"current_tool\": \"terminal\",\n+    }\n+    return parent\n+\n+\n+# ──────────────────────────────────────────────────────────────────────\n+# _agent_has_active_subagents\n+# ──────────────────────────────────────────────────────────────────────\n+class TestAgentHasActiveSubagents:\n+    \"\"\"The detection helper must be both precise and defensive.\"\"\"\n+\n+    def test_returns_false_for_none(self) -> None:\n+        assert GatewayRunner._agent_has_active_subagents(None) is False\n+\n+    def test_returns_false_for_pending_sentinel(self) -> None:\n+        assert (\n+            GatewayRunner._agent_has_active_subagents(_AGENT_PENDING_SENTINEL)\n+            is False\n+        )\n+\n+    def test_returns_false_when_attribute_missing(self) -> None:\n+        \"\"\"Production AIAgents always have _active_children, but the helper\n+        must not blow up on test stubs or partial mocks.\"\"\"\n+\n+        class StubAgent:\n+            pass\n+\n+        assert GatewayRunner._agent_has_active_subagents(StubAgent()) is False\n+\n+    def test_returns_false_for_empty_list(self) -> None:\n+        assert (\n+            GatewayRunner._agent_has_active_subagents(_make_parent_no_subagents())\n+            is False\n+        )\n+\n+    def test_returns_true_for_single_child(self) -> None:\n+        assert (\n+            GatewayRunner._agent_has_active_subagents(_make_parent_with_subagents())\n+            is True\n+        )\n+\n+    def test_returns_true_for_many_children(self) -> None:\n+        assert (\n+            GatewayRunner._agent_has_active_subagents(\n+                _make_parent_with_subagents(children=5)\n+            )\n+            is True\n+        )\n+\n+    def test_works_without_lock(self) -> None:\n+        \"\"\"``_active_children_lock`` is optional in test stubs.\"\"\"\n+        assert (\n+            GatewayRunner._agent_has_active_subagents(\n+                _make_parent_with_subagents(with_lock=False)\n+            )\n+            is True\n+        )\n+\n+    def test_rejects_truthy_non_collection_attribute(self) -> None:\n+        \"\"\"The MagicMock auto-attribute regression. ``MagicMock()._active_children``\n+        is itself a truthy MagicMock — without the isinstance guard, the\n+        helper would falsely report subagents on every test mock.\"\"\"\n+        parent = MagicMock()  # no explicit _active_children setup\n+        assert GatewayRunner._agent_has_active_subagents(parent) is False\n+\n+    @pytest.mark.parametrize(\n+        \"container\",\n+        [(MagicMock(),), {MagicMock()}, [MagicMock()]],\n+        ids=[\"tuple\", \"set\", \"list\"],\n+    )\n+    def test_accepts_list_tuple_set(self, container: Any) -> None:\n+        parent = MagicMock()\n+        parent._active_children = container\n+        parent._active_children_lock = threading.Lock()\n+        assert GatewayRunner._agent_has_active_subagents(parent) is True\n+\n+\n+# ──────────────────────────────────────────────────────────────────────\n+# _handle_active_session_busy_message — interrupt demotion\n+# ──────────────────────────────────────────────────────────────────────\n+class TestBusyHandlerDemotesInterruptForSubagents:\n+    \"\"\"The Phase-1 fix from #30170: parent.interrupt() must NOT fire when\n+    the parent is currently driving subagents.\"\"\"\n+\n+    @pytest.mark.asyncio\n+    async def test_does_not_call_interrupt_when_subagents_active(self) -> None:\n+        runner = _make_runner()\n+        runner._busy_input_mode = \"interrupt\"\n+        adapter = _make_adapter()\n+        event = _make_event(text=\"follow up while subagent runs\")\n+        sk = build_session_key(event.source)\n+        parent = _make_parent_with_subagents()\n+        runner._running_agents[sk] = parent\n+        runner.adapters[event.source.platform] = adapter\n+\n+        with patch(\"gateway.run.merge_pending_message_event\") as merge_mock:\n+            handled = await runner._handle_active_session_busy_message(event, sk)\n+\n+        assert handled is True\n+        parent.interrupt.assert_not_called()\n+        # Message must still be queued so it gets picked up on the next turn.\n+        merge_mock.assert_called_once()\n+\n+    @pytest.mark.asyncio\n+    async def test_ack_explains_the_demotion(self) -> None:\n+        \"\"\"The user-visible ack must mention the subagent context AND\n+        the `/stop` escape hatch so the operator can self-correct.\"\"\"\n+        runner = _make_runner()\n+        runner._busy_input_mode = \"interrupt\"\n+        adapter = _make_adapter()\n+        event = _make_event(text=\"hi mid-delegation\")\n+        sk = build_session_key(event.source)\n+        parent = _make_parent_with_subagents()\n+        runner._running_agents[sk] = parent\n+        runner._running_agents_ts[sk] = time.time() - 120\n+        runner.adapters[event.source.platform] = adapter\n+\n+        with patch(\"gateway.run.merge_pending_message_event\"):\n+            await runner._handle_active_session_busy_message(event, sk)\n+\n+        adapter._send_with_retry.assert_called_once()\n+        content = adapter._send_with_retry.call_args.kwargs.get(\"content\", \"\")\n+        assert \"Subagent working\" in content\n+        assert \"queued\" in content.lower()\n+        assert \"/stop\" in content\n+        assert \"Interrupting\" not in content\n+\n+    @pytest.mark.asyncio\n+    async def test_interrupt_still_fires_when_no_subagents(self) -> None:\n+        \"\"\"Regression-guard the other direction: with no subagents the\n+        demotion must NOT trigger and behaviour must be byte-identical\n+        to the pre-#30170 interrupt path.\"\"\"\n+        runner = _make_runner()\n+        runner._busy_input_mode = \"interrupt\"\n+        adapter = _make_adapter()\n+        event = _make_event(text=\"please stop\")\n+        sk = build_session_key(event.source)\n+        parent = _make_parent_no_subagents()\n+        runner._running_agents[sk] = parent\n+        runner.adapters[event.source.platform] = adapter\n+\n+        with patch(\"gateway.run.merge_pending_message_event\"):\n+            await runner._handle_active_session_busy_message(event, sk)\n+\n+        parent.interrupt.assert_called_once_with(\"please stop\")\n+        content = adapter._send_with_retry.call_args.kwargs.get(\"content\", \"\")\n+        assert \"Interrupting\" in content\n+        assert \"Subagent\" not in content\n+\n+    @pytest.mark.asyncio\n+    async def test_queue_mode_unchanged_with_subagents(self) -> None:\n+        \"\"\"Configured ``queue`` mode is already subagent-safe; the new\n+        guard must not change its behaviour or its ack text.\"\"\"\n+        runner = _make_runner()\n+        runner._busy_input_mode = \"queue\"\n+        adapter = _make_adapter()\n+        event = _make_event(text=\"queued during delegate\")\n+        sk = build_session_key(event.source)\n+        parent = _make_parent_with_subagents()\n+        runner._running_agents[sk] = parent\n+        runner.adapters[event.source.platform] = adapter\n+\n+        with patch(\"gateway.run.merge_pending_message_event\"):\n+            await runner._handle_active_session_busy_message(event, sk)\n+\n+        parent.interrupt.assert_not_called()\n+        content = adapter._send_with_retry.call_args.kwargs.get(\"content\", \"\")\n+        # The vanilla queue copy — NOT the #30170 \"Subagent working\" copy,\n+        # because the user explicitly asked for queue mode.\n+        assert \"Queued for the next turn\" in content\n+        assert \"respond once the current task finishes\" in content\n+        assert \"Subagent working\" not in content\n+\n+    @pytest.mark.asyncio\n+    async def test_steer_mode_still_routes_through_running_agent_steer(\n+        self,\n+    ) -> None:\n+        \"\"\"Configured ``steer`` mode must reach ``running_agent.steer()``\n+        even when subagents are active — the #30170 demotion is\n+        interrupt-specific so it doesn't accidentally disable steer.\"\"\"\n+        runner = _make_runner()\n+        runner._busy_input_mode = \"steer\"\n+        adapter = _make_adapter()\n+        event = _make_event(text=\"course-correct\")\n+        sk = build_session_key(event.source)\n+        parent = _make_parent_with_subagents()\n+        parent.steer = MagicMock(return_value=True)\n+        runner._running_agents[sk] = parent\n+        runner.adapters[event.source.platform] = adapter\n+\n+        with patch(\"gateway.run.merge_pending_message_event\"):\n+            await runner._handle_active_session_busy_message(event, sk)\n+\n+        parent.steer.assert_called_once_with(\"course-correct\")\n+        parent.interrupt.assert_not_called()\n+\n+    @pytest.mark.asyncio\n+    async def test_pending_sentinel_does_not_demote(self) -> None:\n+        \"\"\"The placeholder ``_AGENT_PENDING_SENTINEL`` is not a real\n+        agent — the guard must not treat it as having subagents.\n+        Otherwise we'd permanently queue messages for sessions that\n+        haven't actually started running yet.\"\"\"\n+        runner = _make_runner()\n+        runner._busy_input_mode = \"interrupt\"\n+        adapter = _make_adapter()\n+        event = _make_event(text=\"follow up before start\")\n+        sk = build_session_key(event.source)\n+        runner._running_agents[sk] = _AGENT_PENDING_SENTINEL\n+        runner.adapters[event.source.platform] = adapter\n+\n+        with patch(\"gateway.run.merge_pending_message_event\"):\n+            handled = await runner._handle_active_session_busy_message(event, sk)\n+\n+        assert handled is True\n+        # Sentinel can't be interrupted (no .interrupt to call) — verify\n+        # that the helper still returns the \"interrupting\" copy because\n+        # demotion did NOT fire (and the sentinel branch in the real\n+        # handler just skips the interrupt call silently).\n+        content = adapter._send_with_retry.call_args.kwargs.get(\"content\", \"\")\n+        assert \"Subagent working\" not in content\n"]}]},{"kind":"pr","number":64038,"title":"fix(desktop): preserve live background subagents across message.start (#64015)","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":65040,"title":"fix(tui): preserve background delegation on busy submit","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/test_tui_gateway_queue_on_busy.py","tools/delegate_tool.py"]},{"kind":"pr","number":67005,"title":"fix(desktop): preserve live background subagents across message.start (rebased, #64015)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#72409","number":72409},{"kind":"pr","value":"#64015","number":64015},{"kind":"pr","value":"#67980","number":67980},{"kind":"pr","value":"#64015","number":64015}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":72409,"title":"fix(desktop): preserve live background subagents across message.start (salvage #67005)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts","apps/desktop/src/store/subagents.test.ts","apps/desktop/src/store/subagents.ts","contributors/emails/[redacted:email]"]}],"trust":{"default_tier":"verified","includes_suggested":false}}