{"repo":"NousResearch/hermes-agent","public_id":"30387","display_label":"Honcho one-shot mode SIGABRT shutdown fixes","summary":"These issues and pull requests center on a SIGABRT (exit 134) crash occurring during teardown of the 'hermes -z' one-shot mode, caused by Honcho memory daemon threads blocked in HTTP I/O during interpreter finalization. Work spans bug reports across platforms, fixes for thread joining and shutdown sequencing, and related Honcho integration features.","subsystem":"agent","counts":{"members":15,"open_issues":3,"open_issues_distinct":3,"open_prs":0,"closed_issues":3,"merged_prs":2,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":67905},"resolution":{"state":"partially_resolved","by":"pr-67905"},"freshness":{"state":"verified","last_verified_at":"2026-09-13T13:35:40+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.14780952380952378},"fix_edges":[{"issue_number":30387,"pr_number":67905,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":33485,"pr_number":109328,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":37632,"pr_number":67905,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":39129,"pr_number":109328,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":60616,"pr_number":67905,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":60616,"pr_number":109328,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":71461,"pr_number":109328,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":97639,"pr_number":109328,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":98936,"pr_number":109328,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":30387,"title":"Bug: hermes -z prints successful response then aborts during shutdown with exit 134","url":null,"state":"closed"},{"kind":"issue","number":33485,"title":"[Bug]: Honcho hybrid memory can leave daemon dialectic threads alive during CLI shutdown, causing intermittent CPython abort","url":null,"state":"closed"},{"kind":"issue","number":37632,"title":"hermes -z one-shot crashes with SIGABRT (exit 134) on exit: Honcho memory daemon threads blocked in httpx I/O during interpreter finalization","url":null,"state":"open"},{"kind":"issue","number":43055,"title":"Bug: hermes -z aborts with SIGABRT during teardown on AL2023","url":null,"state":"open"},{"kind":"issue","number":60502,"title":"hermes -z one-shot: correct output emitted, then SIGABRT (exit 134) at teardown when fallback provider is configured (v0.18.0)","url":null,"state":"open"},{"kind":"issue","number":60616,"title":"hermes -z (--oneshot) crashes with SIGABRT after response, only when memory.provider=honcho","url":null,"state":"closed"},{"kind":"pr","number":11680,"title":"fix: cleanly shut down Honcho background workers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"924bc67ee","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":33543,"title":"fix(cli): prevent SIGABRT on hermes -z process exit","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"244dabbd9","number":0},{"kind":"pr","value":"#67905","number":67905}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":33701,"title":"fix(memory/honcho): align shutdown join timeout with Honcho HTTP timeout","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"476dbfed3a","number":0},{"kind":"pr","value":"#109328","number":109328}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":37635,"title":"fix(oneshot): join memory daemon threads before process exit (#37632)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"bfa7a794cb2","number":0},{"kind":"commit","value":"244dabbd9","number":0},{"kind":"commit","value":"7e53b3ef82c","number":0},{"kind":"pr","value":"#67905","number":67905}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":43698,"title":"fix(cli): avoid one-shot SIGABRT during teardown (#43055)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#67905","number":67905}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":62550,"title":"fix(cli): shut down memory provider in oneshot mode to prevent SIGABRT","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#67905","number":67905},{"kind":"pr","value":"#109328","number":109328},{"kind":"pr","value":"#60616","number":60616}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":67768,"title":"fix(cli): clean up oneshot resources before exit","url":null,"state":"closed","close_reason":{"category":"volume_quota","implication":"active"},"merged":false},{"kind":"pr","number":67905,"title":"fix(cli): prevent SIGABRT (exit 134) during hermes -z oneshot teardown","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","hermes_cli/oneshot.py","tests/hermes_cli/test_tui_resume_flow.py"],"fix_hunks":[{"file":"hermes_cli/main.py","hunks":["@@ -65,6 +65,125 @@\n import sys\n \n \n+def _exit_after_oneshot(rc: object) -> None:\n+    \"\"\"Exit one-shot mode without letting late native finalizers change rc.\n+\n+    The SIGABRT this guards against (#30387, #43055) fires in a\n+    native-extension finalizer during CPython's ``Py_FinalizeEx``, *after*\n+    the response has printed. Flush streams, shut down file logging, then\n+    ``os._exit`` past interpreter finalization. The ``atexit`` chain is\n+    deliberately skipped — several handlers re-enter native code that may\n+    be the abort source. Stateful cleanup is handled in ``_run_agent`` and\n+    ``_cleanup_oneshot_runtime``.\n+    \"\"\"\n+    for stream in (sys.stdout, sys.stderr):\n+        try:\n+            stream.flush()\n+        except Exception:\n+            pass\n+    try:\n+        logging.shutdown()\n+    except Exception:\n+        pass\n+    if rc is None:\n+        exit_code = 0\n+    elif isinstance(rc, int):\n+        exit_code = rc\n+    else:\n+        exit_code = 1\n+    os._exit(exit_code)\n+\n+\n+_oneshot_cleanup_done = False\n+\n+\n+def _cleanup_oneshot_runtime() -> None:\n+    \"\"\"Best-effort process-global cleanup before one-shot hard exit.\n+\n+    ``run_oneshot`` owns the agent-local cleanup (memory provider, agent.close,\n+    session_db.close — all in ``_run_agent``'s finally block). This mirrors the\n+    process-global pieces from ``cli.py:_run_cleanup()`` that would otherwise\n+    be skipped by ``os._exit``.\n+    \"\"\"\n+    global _oneshot_cleanup_done\n+    if _oneshot_cleanup_done:\n+        return\n+    _oneshot_cleanup_done = True\n+    try:\n+        from tools.terminal_tool import cleanup_all_environments\n+        cleanup_all_environments()\n+    except Exception:\n+        pass\n+    try:\n+        from tools.async_delegation import interrupt_all\n+        interrupt_all(reason=\"oneshot shutdown\")\n+    except Exception:\n+        pass\n+    try:\n+        from tools.browser_tool import _emergency_cleanup_all_sessions\n+        _emergency_cleanup_all_sessions()\n+    except Exception:\n+        pass\n+    try:\n+        from tools.mcp_tool import shutdown_mcp_servers\n+        shutdown_mcp_servers()\n+    except BaseException:\n+        pass\n+    try:\n+        from agent.auxiliary_client import shutdown_cached_clients\n+        shutdown_cached_clients()\n+    except Exception:\n+        pass\n+\n+\n+def _run_and_exit_oneshot(\n+    prompt: str,\n+    *,\n+    model: object = None,\n+    provider: object = None,\n+    toolsets: object = None,\n+    usage_file: object = None,\n+) -> None:\n+    try:\n+        from hermes_cli.oneshot import run_oneshot\n+\n+        rc = run_oneshot(\n+            prompt,\n+            model=model,\n+            provider=provider,\n+            toolsets=toolsets,\n+            usage_file=usage_file,\n+        )\n+    except KeyboardInterrupt:\n+        rc = 130\n+    except SystemExit as exc:\n+        if exc.code is not None and not isinstance(exc.code, int):\n+            print(exc.code, file=sys.stderr)\n+            rc = 1\n+        else:\n+            rc = exc.code\n+    except BaseException:\n+        # Defense-in-depth. ``run_oneshot`` already converts agent failures\n+        # into an int return code and only re-raises KeyboardInterrupt /\n+        # SystemExit (handled above). Anything still escaping here means\n+        # ``run_oneshot`` itself malfunctioned — surface it on stderr but never\n+        # fall through to normal interpreter teardown, which is the exact path\n+        # that aborts with SIGABRT on AL2023 (the bug this routine fixes).\n+        import traceback\n+        try:\n+            traceback.print_exc()\n+        except Exception:\n+            pass\n+        rc = 1\n+    try:\n+        _cleanup_oneshot_runtime()\n+    finally:\n+        # The hard exit is the safety boundary for #43055. Even an interrupt\n+        # during best-effort cleanup must not fall back into interpreter\n+        # finalization, where the reported native SIGABRT occurs.\n+        _exit_after_oneshot(rc)\n+\n+\n def _set_process_title() -> None:\n     \"\"\"Set the process title to 'hermes' so tools like 'ps', 'top', and\n     'htop' show the app name instead of 'python3.xx'.","@@ -12973,16 +13092,12 @@ def _try_termux_fast_cli_launch() -> bool:\n \n     if getattr(args, \"oneshot\", None):\n         _prepare_agent_startup(args)\n-        from hermes_cli.oneshot import run_oneshot\n-\n-        sys.exit(\n-            run_oneshot(\n-                args.oneshot,\n-                model=getattr(args, \"model\", None),\n-                provider=getattr(args, \"provider\", None),\n-                toolsets=getattr(args, \"toolsets\", None),\n-                usage_file=getattr(args, \"usage_file\", None),\n-            )\n+        _run_and_exit_oneshot(\n+            args.oneshot,\n+            model=getattr(args, \"model\", None),\n+            provider=getattr(args, \"provider\", None),\n+            toolsets=getattr(args, \"toolsets\", None),\n+            usage_file=getattr(args, \"usage_file\", None),\n         )\n \n     if (args.resume or args.continue_last) and args.command is None:","@@ -15130,16 +15245,12 @@ def _export_one(session_id: str):\n     # Handle top-level --oneshot / -z: single-shot mode, stdout = final\n     # response only, nothing else. Bypasses cli.py entirely.\n     if getattr(args, \"oneshot\", None):\n-        from hermes_cli.oneshot import run_oneshot\n-\n-        sys.exit(\n-            run_oneshot(\n-                args.oneshot,\n-                model=getattr(args, \"model\", None),\n-                provider=getattr(args, \"provider\", None),\n-                toolsets=getattr(args, \"toolsets\", None),\n-                usage_file=getattr(args, \"usage_file\", None),\n-            )\n+        _run_and_exit_oneshot(\n+            args.oneshot,\n+            model=getattr(args, \"model\", None),\n+            provider=getattr(args, \"provider\", None),\n+            toolsets=getattr(args, \"toolsets\", None),\n+            usage_file=getattr(args, \"usage_file\", None),\n         )\n \n     # Handle top-level --resume / --continue as shortcut to chat"]},{"file":"hermes_cli/oneshot.py","hunks":["@@ -188,7 +188,7 @@ def run_oneshot(\n             run — even when the run fails — so pipelines can account for\n             spend per invocation.\n \n-    Returns the exit code.  Caller should sys.exit() with the return.\n+    Returns the exit code.  The caller owns process termination.\n     \"\"\"\n     # Silence every stdlib logger for the duration.  AIAgent, tools, and\n     # provider adapters all log to stderr through the root logger; file","@@ -396,44 +396,76 @@ def _run_agent(\n         toolsets_list = sorted(_get_platform_tools(cfg, \"cli\"))\n \n     session_db = _create_session_db_for_oneshot()\n-    # Read the effective fallback chain from profile config so oneshot workers\n-    # honour the same merge semantics as interactive CLI and gateway sessions.\n-    _fb = get_fallback_chain(cfg)\n-\n-    agent = AIAgent(\n-        api_key=[redacted:secret-assignment]\"api_key\"),\n-        base_url=runtime.get(\"base_url\"),\n-        provider=runtime.get(\"provider\"),\n-        api_mode=runtime.get(\"api_mode\"),\n-        model=effective_model,\n-        enabled_toolsets=toolsets_list,\n-        quiet_mode=True,\n-        platform=\"cli\",\n-        session_db=session_db,\n-        credential_pool=[redacted:secret-assignment]\"credential_pool\"),\n-        fallback_model=_fb or None,\n-        # Interactive callbacks are intentionally NOT wired beyond this\n-        # one.  In oneshot mode there's no user sitting at a terminal:\n-        #   - clarify  → returns a synthetic \"pick a default\" instruction\n-        #                so the agent continues instead of stalling on\n-        #                the tool's built-in \"not available\" error\n-        #   - sudo password prompt → terminal_tool gates on\n-        #                HERMES_INTERACTIVE which we never set\n-        #   - shell-hook approval → auto-approved via HERMES_ACCEPT_HOOKS=1\n-        #                (set above); also falls back to deny on non-tty\n-        #   - dangerous-command approval → bypassed via HERMES_YOLO_MODE=1\n-        #   - skill secret capture → returns gracefully when no callback set\n-        clarify_callback=_oneshot_clarify_callback,\n-    )\n+    # The try spans agent construction (not just ``chat``) so the SQLite store\n+    # opened above is always closed — including when ``AIAgent(...)`` itself\n+    # raises on a provider/config error. The one-shot exit path hard-exits via\n+    # os._exit and skips finalizers, so an un-closed connection here would leak.\n+    agent = None\n+    try:\n+        # Read the effective fallback chain from profile config so oneshot\n+        # workers honour the same merge semantics as interactive CLI and\n+        # gateway sessions.\n+        _fb = get_fallback_chain(cfg)\n+\n+        agent = AIAgent(\n+            api_key=[redacted:secret-assignment]\"api_key\"),\n+            base_url=runtime.get(\"base_url\"),\n+            provider=runtime.get(\"provider\"),\n+            api_mode=runtime.get(\"api_mode\"),\n+            model=effective_model,\n+            enabled_toolsets=toolsets_list,\n+            quiet_mode=True,\n+            platform=\"cli\",\n+            session_db=session_db,\n+            credential_pool=[redacted:secret-assignment]\"credential_pool\"),\n+            fallback_model=_fb or None,\n+            # Interactive callbacks are intentionally NOT wired beyond this\n+            # one.  In oneshot mode there's no user sitting at a terminal:\n+            #   - clarify  → returns a synthetic \"pick a default\" instruction\n+            #                so the agent continues instead of stalling on\n+            #                the tool's built-in \"not available\" error\n+            #   - sudo password prompt → terminal_tool gates on\n+            #                HERMES_INTERACTIVE which we never set\n+            #   - shell-hook approval → auto-approved via HERMES_ACCEPT_HOOKS=1\n+            #                (set above); also falls back to deny on non-tty\n+            #   - dangerous-command approval → bypassed via HERMES_YOLO_MODE=1\n+            #   - skill secret capture → returns gracefully when no callback set\n+            clarify_callback=_oneshot_clarify_callback,\n+        )\n \n-    # Belt-and-braces: make sure AIAgent doesn't invoke any streaming\n-    # display callbacks that would bypass our stdout capture.\n-    agent.suppress_status_output = True\n-    agent.stream_delta_callback = None\n-    agent.tool_gen_callback = None\n+        # Belt-and-braces: make sure AIAgent doesn't invoke any streaming\n+        # display callbacks that would bypass our stdout capture.\n+        agent.suppress_status_output = True\n+        agent.stream_delta_callback = None\n+        agent.tool_gen_callback = None\n \n-    result = agent.run_conversation(prompt)\n-    return (result.get(\"final_response\") or \"\", result)\n+        result = agent.run_conversation(prompt)\n+        return (result.get(\"final_response\") or \"\", result)\n+    finally:\n+        # Ordering deliberately mirrors gateway/run.py:_cleanup_agent_resources,\n+        # NOT cli.py:_run_cleanup — oneshot has no _active_agent_ref and must\n+        # close the agent explicitly because the hard-exit path skips finalizers.\n+        if agent is not None:\n+            try:\n+                session_messages = getattr(agent, \"_session_messages\", None)\n+                if isinstance(session_messages, list):\n+                    agent.shutdown_memory_provider(session_messages)\n+                else:\n+                    agent.shutdown_memory_provider()\n+            except Exception:\n+                logging.debug(\"oneshot memory/context cleanup failed\", exc_info=True)\n+            try:\n+                agent.close()\n+            except Exception:\n+                logging.debug(\"oneshot agent cleanup failed\", exc_info=True)\n+        # agent.close() calls session_db.end_session() but leaves the connection\n+        # open; close it here to checkpoint the WAL before os._exit skips\n+        # finalizers.\n+        if session_db is not None:\n+            try:\n+                session_db.close()\n+            except Exception:\n+                logging.debug(\"oneshot session store cleanup failed\", exc_info=True)\n \n \n def _oneshot_clarify_callback(question: str, choices=None) -> str:"]},{"file":"tests/hermes_cli/test_tui_resume_flow.py","hunks":["@@ -1,7 +1,9 @@\n from argparse import Namespace\n import os\n from pathlib import Path\n+import subprocess\n import sys\n+import textwrap\n import types\n \n import pytest","@@ -21,11 +23,17 @@ def _args(**overrides):\n     return Namespace(**base)\n \n \n+def _raise_exit(rc):\n+    raise SystemExit(rc)\n+\n+\n @pytest.fixture\n def main_mod(monkeypatch):\n     import hermes_cli.main as mod\n \n     monkeypatch.setattr(mod, \"_has_any_provider_configured\", lambda: True)\n+    # Reset the idempotency guard so each test starts fresh.\n+    monkeypatch.setattr(mod, \"_oneshot_cleanup_done\", False)\n     return mod\n \n ","@@ -353,7 +361,17 @@ def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod)\n     monkeypatch.setattr(\n         sys,\n         \"argv\",\n-        [\"hermes\", \"-z\", \"hello\", \"--model\", \"gpt-test\", \"--provider\", \"openai\"],\n+        [\n+            \"hermes\",\n+            \"-z\",\n+            \"hello\",\n+            \"--model\",\n+            \"gpt-test\",\n+            \"--provider\",\n+            \"openai\",\n+            \"--usage-file\",\n+            \"usage.json\",\n+        ],\n     )\n     monkeypatch.setattr(\n         main_mod, \"_prepare_agent_startup\", lambda args: prepared.append(args.command)","@@ -368,6 +386,11 @@ def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod)\n             or 17\n         ),\n     )\n+    monkeypatch.setattr(\n+        main_mod,\n+        \"_exit_after_oneshot\",\n+        _raise_exit,\n+    )\n \n     with pytest.raises(SystemExit) as exc:\n         main_mod._try_termux_fast_cli_launch()","@@ -379,7 +402,7 @@ def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod)\n         \"model\": \"gpt-test\",\n         \"provider\": \"openai\",\n         \"toolsets\": None,\n-        \"usage_file\": None,\n+        \"usage_file\": \"usage.json\",\n     }\n \n ","@@ -577,7 +600,17 @@ def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):\n     import hermes_cli.config as config_mod\n \n     monkeypatch.setattr(\n-        sys, \"argv\", [\"hermes\", \"-z\", \"hello\", \"--toolsets\", \"web,terminal\"]\n+        sys,\n+        \"argv\",\n+        [\n+            \"hermes\",\n+            \"-z\",\n+            \"hello\",\n+            \"--toolsets\",\n+            \"web,terminal\",\n+            \"--usage-file\",\n+            \"usage.json\",\n+        ],\n     )\n     monkeypatch.setitem(\n         sys.modules,","@@ -608,6 +641,11 @@ def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):\n             or 0\n         ),\n     )\n+    monkeypatch.setattr(\n+        main_mod,\n+        \"_exit_after_oneshot\",\n+        _raise_exit,\n+    )\n \n     with pytest.raises(SystemExit) as exc:\n         main_mod.main()","@@ -618,10 +656,721 @@ def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):\n         \"model\": None,\n         \"provider\": None,\n         \"toolsets\": \"web,terminal\",\n-        \"usage_file\": None,\n+        \"usage_file\": \"usage.json\",\n     }\n \n \n+def test_exit_after_oneshot_flushes_stdio_and_calls_os_exit(\n+    monkeypatch, main_mod\n+):\n+    flushed = []\n+    exits = []\n+\n+    class FakeStream:\n+        def __init__(self, name):\n+            self.name = name\n+\n+        def flush(self):\n+            flushed.append(self.name)\n+\n+    def fake_exit(rc):\n+        exits.append(rc)\n+        raise SystemExit(rc)\n+\n+    monkeypatch.setattr(main_mod.sys, \"stdout\", FakeStream(\"stdout\"))\n+    monkeypatch.setattr(main_mod.sys, \"stderr\", FakeStream(\"stderr\"))\n+    monkeypatch.setattr(main_mod.os, \"_exit\", fake_exit)\n+    monkeypatch.setattr(\"logging.shutdown\", lambda: None)\n+\n+    with pytest.raises(SystemExit) as exc:\n+        main_mod._exit_after_oneshot(17)\n+\n+    assert exc.value.code == 17\n+    assert exits == [17]\n+    assert flushed == [\"stdout\", \"stderr\"]\n+\n+\n+def test_exit_after_oneshot_invokes_logging_shutdown_in_order(\n+    monkeypatch, main_mod\n+):\n+    events = []\n+\n+    class FakeStream:\n+        def __init__(self, name):\n+            self.name = name\n+\n+        def flush(self):\n+            events.append(f\"flush:{self.name}\")\n+\n+    def fake_exit(rc):\n+        events.append(f\"exit:{rc}\")\n+        raise SystemExit(rc)\n+\n+    monkeypatch.setattr(main_mod.sys, \"stdout\", FakeStream(\"stdout\"))\n+    monkeypatch.setattr(main_mod.sys, \"stderr\", FakeStream(\"stderr\"))\n+    monkeypatch.setattr(\"logging.shutdown\", lambda: events.append(\"shutdown\"))\n+    monkeypatch.setattr(main_mod.os, \"_exit\", fake_exit)\n+\n+    with pytest.raises(SystemExit) as exc:\n+        main_mod._exit_after_oneshot(0)\n+\n+    assert exc.value.code == 0\n+    assert events == [\"flush:stdout\", \"flush:stderr\", \"shutdown\", \"exit:0\"]\n+\n+\n+def test_exit_after_oneshot_exits_even_if_logging_shutdown_raises(\n+    monkeypatch, main_mod\n+):\n+    exits = []\n+\n+    def fake_exit(rc):\n+        exits.append(rc)\n+        raise SystemExit(rc)\n+\n+    monkeypatch.setattr(\n+        main_mod.sys, \"stdout\", types.SimpleNamespace(flush=lambda: None)\n+    )\n+    monkeypatch.setattr(\n+        main_mod.sys, \"stderr\", types.SimpleNamespace(flush=lambda: None)\n+    )\n+    monkeypatch.setattr(\n+        \"logging.shutdown\",\n+        lambda: (_ for _ in ()).throw(RuntimeError(\"shutdown failed\")),\n+    )\n+    monkeypatch.setattr(main_mod.os, \"_exit\", fake_exit)\n+\n+    with pytest.raises(SystemExit) as exc:\n+        main_mod._exit_after_oneshot(1)\n+\n+    assert exc.value.code == 1\n+    assert exits == [1]\n+\n+\n+def test_exit_after_oneshot_flushes_stderr_when_stdout_flush_fails(\n+    monkeypatch, main_mod\n+):\n+    flushed = []\n+    exits = []\n+\n+    class BadStdout:\n+        def flush(self):\n+            raise BrokenPipeError(\"pipe closed\")\n+\n+    class FakeStderr:\n+        def flush(self):\n+            flushed.append(\"stderr\")\n+\n+    def fake_exit(rc):\n+        exits.append(rc)\n+        raise SystemExit(rc)\n+\n+    monkeypatch.setattr(main_mod.sys, \"stdout\", BadStdout())\n+    monkeypatch.setattr(main_mod.sys, \"stderr\", FakeStderr())\n+    monkeypatch.setattr(main_mod.os, \"_exit\", fake_exit)\n+    monkeypatch.setattr(\"logging.shutdown\", lambda: None)\n+\n+    with pytest.raises(SystemExit) as exc:\n+        main_mod._exit_after_oneshot(2)\n+\n+    assert exc.value.code == 2\n+    assert exits == [2]\n+    assert flushed == [\"stderr\"]\n+\n+\n+def test_exit_after_oneshot_normalizes_non_int_exit_code(monkeypatch, main_mod):\n+    exits = []\n+\n+    def fake_exit(rc):\n+        exits.append(rc)\n+        raise SystemExit(rc)\n+\n+    monkeypatch.setattr(main_mod.os, \"_exit\", fake_exit)\n+    monkeypatch.setattr(\"logging.shutdown\", lambda: None)\n+\n+    with pytest.raises(SystemExit) as exc:\n+        main_mod._exit_after_oneshot(None)\n+\n+    assert exc.value.code == 0\n+    assert exits == [0]\n+\n+\n+def test_run_and_exit_oneshot_routes_system_exit_to_hard_exit(monkeypatch, main_mod):\n+    exits = []\n+\n+    def fake_run_oneshot(*_args, **_kwargs):\n+        raise SystemExit(2)\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=fake_run_oneshot),\n+    )\n+    monkeypatch.setattr(main_mod, \"_cleanup_oneshot_runtime\", lambda: None)\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", lambda rc: exits.append(rc))\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert exits == [2]\n+\n+\n+def test_run_and_exit_oneshot_routes_bare_system_exit_to_zero(monkeypatch, main_mod):\n+    exits = []\n+\n+    def fake_run_oneshot(*_args, **_kwargs):\n+        raise SystemExit\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=fake_run_oneshot),\n+    )\n+    monkeypatch.setattr(main_mod, \"_cleanup_oneshot_runtime\", lambda: None)\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", lambda rc: exits.append(rc))\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert exits == [None]\n+\n+\n+def test_run_and_exit_oneshot_prints_system_exit_message(\n+    monkeypatch, capsys, main_mod\n+):\n+    exits = []\n+\n+    def fake_run_oneshot(*_args, **_kwargs):\n+        raise SystemExit(\"fatal\")\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=fake_run_oneshot),\n+    )\n+    monkeypatch.setattr(main_mod, \"_cleanup_oneshot_runtime\", lambda: None)\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", lambda rc: exits.append(rc))\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert exits == [1]\n+    captured = capsys.readouterr()\n+    assert captured.out == \"\"\n+    assert captured.err == \"fatal\\n\"\n+\n+\n+def test_run_and_exit_oneshot_cleans_global_runtime_before_hard_exit(\n+    monkeypatch, main_mod\n+):\n+    events = []\n+\n+    def _mod(name, **attrs):\n+        fake = types.ModuleType(name)\n+        for key, value in attrs.items():\n+            setattr(fake, key, value)\n+        return fake\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: events.append(\"run\") or 0),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.terminal_tool\",\n+        _mod(\"tools.terminal_tool\", cleanup_all_environments=lambda: events.append(\"terminal\")),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.async_delegation\",\n+        _mod(\"tools.async_delegation\", interrupt_all=lambda **kw: events.append(\"delegation\")),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.browser_tool\",\n+        _mod(\"tools.browser_tool\", _emergency_cleanup_all_sessions=lambda: events.append(\"browser\")),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.mcp_tool\",\n+        _mod(\"tools.mcp_tool\", shutdown_mcp_servers=lambda: events.append(\"mcp\")),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"agent.auxiliary_client\",\n+        _mod(\"agent.auxiliary_client\", shutdown_cached_clients=lambda: events.append(\"aux\")),\n+    )\n+    monkeypatch.setattr(\n+        main_mod, \"_exit_after_oneshot\", lambda rc: events.append(f\"exit:{rc}\")\n+    )\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert events == [\"run\", \"terminal\", \"delegation\", \"browser\", \"mcp\", \"aux\", \"exit:0\"]\n+\n+\n+def test_run_and_exit_oneshot_still_exits_when_global_cleanup_raises(\n+    monkeypatch, main_mod\n+):\n+    events = []\n+\n+    def _raise_mcp():\n+        raise RuntimeError(\"mcp boom\")\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.terminal_tool\",\n+        types.SimpleNamespace(cleanup_all_environments=lambda: events.append(\"terminal\")),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.async_delegation\",\n+        types.SimpleNamespace(interrupt_all=lambda **kw: events.append(\"delegation\")),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.browser_tool\",\n+        types.SimpleNamespace(_emergency_cleanup_all_sessions=lambda: events.append(\"browser\")),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"tools.mcp_tool\",\n+        types.SimpleNamespace(shutdown_mcp_servers=_raise_mcp),\n+    )\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"agent.auxiliary_client\",\n+        types.SimpleNamespace(shutdown_cached_clients=lambda: events.append(\"aux\")),\n+    )\n+    monkeypatch.setattr(\n+        main_mod, \"_exit_after_oneshot\", lambda rc: events.append(f\"exit:{rc}\")\n+    )\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert events == [\"terminal\", \"delegation\", \"browser\", \"aux\", \"exit:0\"]\n+\n+\n+def test_run_and_exit_oneshot_hard_exits_when_cleanup_is_interrupted(\n+    monkeypatch, main_mod\n+):\n+    def _raise_keyboard_interrupt():\n+        raise KeyboardInterrupt\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0),\n+    )\n+    monkeypatch.setattr(\n+        main_mod, \"_cleanup_oneshot_runtime\", _raise_keyboard_interrupt\n+    )\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", _raise_exit)\n+\n+    with pytest.raises(SystemExit) as exc:\n+        main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert exc.value.code == 0\n+\n+\n+def test_run_and_exit_oneshot_routes_keyboard_interrupt_to_130(\n+    monkeypatch, main_mod\n+):\n+    exits = []\n+\n+    def fake_run_oneshot(*_args, **_kwargs):\n+        raise KeyboardInterrupt\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=fake_run_oneshot),\n+    )\n+    monkeypatch.setattr(main_mod, \"_cleanup_oneshot_runtime\", lambda: None)\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", lambda rc: exits.append(rc))\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert exits == [130]\n+\n+\n+def test_run_and_exit_oneshot_hard_exits_on_unexpected_exception(\n+    monkeypatch, main_mod, capsys\n+):\n+    # ``run_oneshot`` is contracted to convert agent failures into an int and\n+    # only re-raise KeyboardInterrupt / SystemExit. If it ever malfunctions and\n+    # lets another exception escape, the one-shot path must still hard-exit\n+    # (rc 1) rather than fall through to interpreter teardown — the exact path\n+    # that SIGABRTs on AL2023.\n+    exits = []\n+    cleaned = []\n+\n+    def fake_run_oneshot(*_args, **_kwargs):\n+        raise RuntimeError(\"run_oneshot itself blew up\")\n+\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=fake_run_oneshot),\n+    )\n+    monkeypatch.setattr(\n+        main_mod, \"_cleanup_oneshot_runtime\", lambda: cleaned.append(True)\n+    )\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", lambda rc: exits.append(rc))\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert exits == [1]\n+    # Global cleanup still runs on the defensive path — resources must not leak\n+    # just because run_oneshot malfunctioned.\n+    assert cleaned == [True]\n+    # The failure is surfaced on stderr, never swallowed silently.\n+    assert \"run_oneshot itself blew up\" in capsys.readouterr().err\n+\n+\n+def test_run_and_exit_oneshot_hard_exits_when_oneshot_import_fails(\n+    monkeypatch, main_mod, capsys\n+):\n+    import builtins\n+\n+    exits = []\n+    cleaned = []\n+    real_import = builtins.__import__\n+\n+    def fake_import(name, globals=None, locals=None, fromlist=(), level=0):\n+        if name == \"hermes_cli.oneshot\" and \"run_oneshot\" in (fromlist or ()):\n+            raise RuntimeError(\"oneshot import blew up\")\n+        return real_import(name, globals, locals, fromlist, level)\n+\n+    monkeypatch.setattr(builtins, \"__import__\", fake_import)\n+    monkeypatch.setattr(\n+        main_mod, \"_cleanup_oneshot_runtime\", lambda: cleaned.append(True)\n+    )\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", lambda rc: exits.append(rc))\n+\n+    main_mod._run_and_exit_oneshot(\"hello\")\n+\n+    assert exits == [1]\n+    assert cleaned == [True]\n+    assert \"oneshot import blew up\" in capsys.readouterr().err\n+\n+\n+def test_oneshot_subprocess_exits_without_teardown_abort():\n+    program = textwrap.dedent(\n+        \"\"\"\n+        import hermes_cli.oneshot as oneshot\n+        from hermes_cli.main import _exit_after_oneshot\n+\n+        oneshot._run_agent = lambda *args, **kwargs: (\"ok\", {\"final_response\": \"ok\"})\n+        _exit_after_oneshot(oneshot.run_oneshot(\"hello\"))\n+        \"\"\"\n+    )\n+\n+    result = subprocess.run(\n+        [sys.executable, \"-c\", program],\n+        cwd=Path(__file__).resolve().parents[2],\n+        capture_output=True,\n+        timeout=10,\n+        check=False,\n+    )\n+\n+    assert result.returncode == 0\n+    assert result.stdout == b\"ok\\n\"\n+    # Don't demand byte-empty stderr — an import-time warning from the heavy\n+    # CLI import chain shouldn't fail this. What matters is no crash traceback.\n+    assert b\"Traceback\" not in result.stderr\n+\n+\n+def test_exit_after_oneshot_bypasses_late_atexit_abort():\n+    program = textwrap.dedent(\n+        \"\"\"\n+        import atexit\n+        import os\n+        import sys\n+        from hermes_cli.main import _exit_after_oneshot\n+\n+        atexit.register(os.abort)\n+        sys.stdout.write(\"done\\\\n\")\n+        _exit_after_oneshot(0)\n+        \"\"\"\n+    )\n+\n+    result = subprocess.run(\n+        [sys.executable, \"-c\", program],\n+        cwd=Path(__file__).resolve().parents[2],\n+        capture_output=True,\n+        timeout=10,\n+        check=False,\n+    )\n+\n+    assert result.returncode == 0\n+    assert result.stdout == b\"done\\n\"\n+\n+\n+def test_run_and_exit_oneshot_passes_through_nonzero_return(monkeypatch, main_mod):\n+    # A non-zero rc from run_oneshot (e.g. provider-without-model → 2, or the\n+    # empty-response guard → 1) must reach os._exit unchanged.\n+    exits = []\n+    monkeypatch.setitem(\n+        sys.modules,\n+        \"hermes_cli.oneshot\",\n+        types.SimpleNamespace(run_oneshot=lambda *a, **k: 2),\n+    )\n+    monkeypatch.setattr(main_mod, \"_cleanup_oneshot_runtime\", lambda: None)\n+    monkeypatch.setattr(main_mod, \"_exit_after_oneshot\", lambda rc: exits.append(rc))\n+\n+    main_mod._run_and_exit_oneshot(\"hi\")\n+\n+    assert exits == [2]\n+\n+\n+def test_main_oneshot_path_bypasses_late_atexit_abort():\n+    # End-to-end through the real top-level ``main()`` ``-z`` path: a valid\n+    # response prints, then a late atexit handler that would abort is bypassed\n+    # by the hard exit, so the process reports success (#43055).\n+    program = textwrap.dedent(\n+        \"\"\"\n+        import atexit\n+        import os\n+        import sys\n+        import types\n+\n+        import hermes_cli.main as main_mod\n+\n+        sys.argv = [\"hermes\", \"-z\", \"hello\"]\n+        main_mod._prepare_agent_startup = lambda args: None\n+\n+        def _fake_run_oneshot(prompt, **kwargs):\n+            print(\"ok\")\n+            return 0\n+\n+        sys.modules[\"hermes_cli.oneshot\"] = types.SimpleNamespace(\n+            run_oneshot=_fake_run_oneshot\n+        )\n+        atexit.register(os.abort)\n+        main_mod.main()\n+        \"\"\"\n+    )\n+\n+    result = subprocess.run(\n+        [sys.executable, \"-c\", program],\n+        cwd=Path(__file__).resolve().parents[2],\n+        capture_output=True,\n+        timeout=30,\n+        check=False,\n+    )\n+\n+    assert result.returncode == 0\n+    assert result.stdout == b\"ok\\n\"\n+    assert b\"Traceback\" not in result.stderr\n+\n+\n+def test_oneshot_run_agent_closes_agent_after_chat(monkeypatch):\n+    import hermes_cli.oneshot as oneshot_mod\n+\n+    closed = []\n+    shutdown_messages = []\n+\n+    class FakeAgent:\n+        def __init__(self, **_kwargs):\n+            self.suppress_status_output = False\n+            self.stream_delta_callback = object()\n+            self.tool_gen_callback = object()\n+            self._session_messages = [{\"role\": \"user\", \"content\": \"hello\"}]\n+\n+        def run_conversation(self, prompt, **_kwargs):\n+            assert prompt == \"hello\"\n+            return {\"final_response\": \"done\"}\n+\n+        def shutdown_memory_provider(self, messages=None):\n+            shutdown_messages.append(messages)\n+\n+        def close(self):\n+            closed.append(True)\n+\n+    monkeypatch.setitem(\n+        sys.modules, \"run_agent\", types.SimpleNamespace(AIAgent=FakeAgent)\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.config.load_config\",\n+        lambda: {\"model\": {\"default\": \"gpt-test\", \"provider\": \"openai\"}},\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+        lambda **_kwargs: {\n+            \"api_key\": \"key\",\n+            \"base_url\": \"https://example.invalid\",\n+            \"provider\": \"openai\",\n+            \"api_mode\": \"chat_completions\",\n+            \"credential_pool\": None,\n+        },\n+    )\n+    monkeypatch.setattr(oneshot_mod, \"_create_session_db_for_oneshot\", lambda: None)\n+\n+    assert (\n+        oneshot_mod._run_agent(\n+            \"hello\", model=\"gpt-test\", provider=\"openai\", use_config_toolsets=False\n+        )\n+        == (\"done\", {\"final_response\": \"done\"})\n+    )\n+    assert closed == [True]\n+    assert shutdown_messages == [[{\"role\": \"user\", \"content\": \"hello\"}]]\n+\n+\n+def test_oneshot_run_agent_closes_agent_when_chat_raises(monkeypatch):\n+    import hermes_cli.oneshot as oneshot_mod\n+\n+    closed = []\n+    shutdowns = []\n+\n+    class FakeAgent:\n+        def __init__(self, **_kwargs):\n+            self.suppress_status_output = False\n+            self.stream_delta_callback = object()\n+            self.tool_gen_callback = object()\n+\n+        def run_conversation(self, _prompt, **_kwargs):\n+            raise RuntimeError(\"boom\")\n+\n+        def shutdown_memory_provider(self, messages=None):\n+            shutdowns.append(messages)\n+\n+        def close(self):\n+            closed.append(True)\n+\n+    monkeypatch.setitem(\n+        sys.modules, \"run_agent\", types.SimpleNamespace(AIAgent=FakeAgent)\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.config.load_config\",\n+        lambda: {\"model\": {\"default\": \"gpt-test\", \"provider\": \"openai\"}},\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+        lambda **_kwargs: {\n+            \"api_key\": \"key\",\n+            \"base_url\": \"https://example.invalid\",\n+            \"provider\": \"openai\",\n+            \"api_mode\": \"chat_completions\",\n+            \"credential_pool\": None,\n+        },\n+    )\n+    monkeypatch.setattr(oneshot_mod, \"_create_session_db_for_oneshot\", lambda: None)\n+\n+    with pytest.raises(RuntimeError, match=\"boom\"):\n+        oneshot_mod._run_agent(\n+            \"hello\", model=\"gpt-test\", provider=\"openai\", use_config_toolsets=False\n+        )\n+    assert closed == [True]\n+    assert shutdowns == [None]\n+\n+\n+def test_oneshot_run_agent_closes_session_db(monkeypatch):\n+    # The one-shot exit path hard-exits via os._exit and skips finalizers, so\n+    # the recall SQLite store it opens must be closed explicitly (checkpointing\n+    # its WAL) rather than left to interpreter teardown.\n+    import hermes_cli.oneshot as oneshot_mod\n+\n+    db_closed = []\n+\n+    class FakeAgent:\n+        def __init__(self, **_kwargs):\n+            self.suppress_status_output = False\n+            self.stream_delta_callback = object()\n+            self.tool_gen_callback = object()\n+            self._session_messages = []\n+\n+        def run_conversation(self, _prompt, **_kwargs):\n+            return {\"final_response\": \"done\"}\n+\n+        def shutdown_memory_provider(self, messages=None):\n+            pass\n+\n+        def close(self):\n+            pass\n+\n+    class FakeSessionDB:\n+        def close(self):\n+            db_closed.append(True)\n+\n+    monkeypatch.setitem(\n+        sys.modules, \"run_agent\", types.SimpleNamespace(AIAgent=FakeAgent)\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.config.load_config\",\n+        lambda: {\"model\": {\"default\": \"gpt-test\", \"provider\": \"openai\"}},\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+        lambda **_kwargs: {\n+            \"api_key\": \"key\",\n+            \"base_url\": \"https://example.invalid\",\n+            \"provider\": \"openai\",\n+            \"api_mode\": \"chat_completions\",\n+            \"credential_pool\": None,\n+        },\n+    )\n+    monkeypatch.setattr(\n+        oneshot_mod, \"_create_session_db_for_oneshot\", lambda: FakeSessionDB()\n+    )\n+\n+    assert (\n+        oneshot_mod._run_agent(\n+            \"hello\", model=\"gpt-test\", provider=\"openai\", use_config_toolsets=False\n+        )\n+        == (\"done\", {\"final_response\": \"done\"})\n+    )\n+    assert db_closed == [True]\n+\n+\n+def test_oneshot_run_agent_closes_session_db_when_agent_init_raises(monkeypatch):\n+    # The recall store is opened before AIAgent is constructed. If construction\n+    # raises (bad provider/config/model), the store must still be closed — the\n+    # one-shot exit hard-exits via os._exit and skips finalizers, so an\n+    # un-closed connection would leave a stale WAL behind.\n+    import hermes_cli.oneshot as oneshot_mod\n+\n+    db_closed = []\n+\n+    class FakeSessionDB:\n+        def close(self):\n+            db_closed.append(True)\n+\n+    class FakeAgent:\n+        def __init__(self, **_kwargs):\n+            raise RuntimeError(\"init boom\")\n+\n+    monkeypatch.setitem(\n+        sys.modules, \"run_agent\", types.SimpleNamespace(AIAgent=FakeAgent)\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.config.load_config\",\n+        lambda: {\"model\": {\"default\": \"gpt-test\", \"provider\": \"openai\"}},\n+    )\n+    monkeypatch.setattr(\n+        \"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+        lambda **_kwargs: {\n+            \"api_key\": \"key\",\n+            \"base_url\": \"https://example.invalid\",\n+            \"provider\": \"openai\",\n+            \"api_mode\": \"chat_completions\",\n+            \"credential_pool\": None,\n+        },\n+    )\n+    monkeypatch.setattr(\n+        oneshot_mod, \"_create_session_db_for_oneshot\", lambda: FakeSessionDB()\n+    )\n+\n+    with pytest.raises(RuntimeError, match=\"init boom\"):\n+        oneshot_mod._run_agent(\n+            \"hello\", model=\"gpt-test\", provider=\"openai\", use_config_toolsets=False\n+        )\n+\n+    assert db_closed == [True]\n+\n+\n def _stub_plugin_discovery(monkeypatch):\n     monkeypatch.setitem(\n         sys.modules,\n"]}]},{"kind":"pr","number":109328,"title":"Honcho gateway identity: wizard copy, 'peers map', sessionAiPeerPrefix (salvage #83981, #39130)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["contributors/emails/[redacted:email]","contributors/emails/[redacted:email]","contributors/emails/[redacted:email]","contributors/emails/[redacted:email]","plugins/memory/honcho/README.md","plugins/memory/honcho/__init__.py","plugins/memory/honcho/cli.py","plugins/memory/honcho/client.py","plugins/memory/honcho/config_schema.py","plugins/memory/honcho/dialectic.py","plugins/memory/honcho/recall_sync.py","plugins/memory/honcho/session.py","plugins/memory/honcho/session_context.py","plugins/memory/honcho/session_migration.py","plugins/memory/honcho/session_peers.py","tests/agent/test_memory_user_id.py","tests/honcho_plugin/test_a2a_sessions.py","tests/honcho_plugin/test_async_memory.py","tests/honcho_plugin/test_auth_recovery.py","tests/honcho_plugin/test_cli.py","tests/honcho_plugin/test_identity_signature.py","tests/honcho_plugin/test_peers_map.py","tests/honcho_plugin/test_pin_peer_name.py","tests/honcho_plugin/test_recall_sync.py","tests/honcho_plugin/test_session.py","tests/honcho_plugin/test_session_ai_peer_prefix.py","tests/honcho_plugin/test_shutdown.py","tests/honcho_plugin/test_turn_author_peers.py","tests/plugins/memory/test_provider_threads_inherit_profile.py","tests/test_honcho_session_cache_bounds.py","tests/test_honcho_session_context.py","tests/test_honcho_startup_fail_open.py","tests/test_honcho_summary_sanitize.py","tests/test_tui_gateway_server.py","tests/tui_gateway/test_compute_host_turn_protocol.py","tui_gateway/compute_host.py","tui_gateway/compute_host_bridge.py","tui_gateway/methods_session.py","tui_gateway/server.py","tui_gateway/session_transports.py","tui_gateway/ws.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}