# NousResearch/hermes-agent · Honcho one-shot mode SIGABRT shutdown fixes

## Open now

- 3 issues, 0 PRs open on GitHub.
- #37632: hermes -z one-shot crashes with SIGABRT (exit 134) on exit: Honcho memory daemon threads blocked in httpx I/O during interpreter finalization
- #43055: Bug: hermes -z aborts with SIGABRT during teardown on AL2023
- #60502: hermes -z one-shot: correct output emitted, then SIGABRT (exit 134) at teardown when fallback provider is configured (v0.18.0)

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.

## TL;DR

- 15 member(s): 3 open issue(s), 3 closed issue(s), 0 open PR(s), 2 merged PR(s), 7 PR(s) closed without merging.
- A fix was merged to main in PR #67905. Verify it addresses your case.
- **Recommended:** read PR #67905 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-67905`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Open related issues

- #37632: hermes -z one-shot crashes with SIGABRT (exit 134) on exit: Honcho memory daemon threads blocked in httpx I/O during interpreter finalization
- #43055: Bug: hermes -z aborts with SIGABRT during teardown on AL2023
- #60502: hermes -z one-shot: correct output emitted, then SIGABRT (exit 134) at teardown when fallback provider is configured (v0.18.0)

## Closed / duplicate issues

- #30387: Bug: hermes -z prints successful response then aborts during shutdown with exit 134
- #33485: \[Bug\]: Honcho hybrid memory can leave daemon dialectic threads alive during CLI shutdown, causing intermittent CPython abort
- #60616: hermes -z (--oneshot) crashes with SIGABRT after response, only when memory.provider=honcho

## Prior & competing PRs

- **#67905: fix(cli): prevent SIGABRT (exit 134) during hermes -z oneshot teardown**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`, `hermes_cli/oneshot.py`, `tests/hermes_cli/test_tui_resume_flow.py`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -65,6 +65,125 @@
     import sys
     
     
    +def _exit_after_oneshot(rc: object) -> None:
    +    """Exit one-shot mode without letting late native finalizers change rc.
    +
    +    The SIGABRT this guards against (#30387, #43055) fires in a
    +    native-extension finalizer during CPython's ``Py_FinalizeEx``, *after*
    +    the response has printed. Flush streams, shut down file logging, then
    +    ``os._exit`` past interpreter finalization. The ``atexit`` chain is
    +    deliberately skipped — several handlers re-enter native code that may
    +    be the abort source. Stateful cleanup is handled in ``_run_agent`` and
    +    ``_cleanup_oneshot_runtime``.
    +    """
    +    for stream in (sys.stdout, sys.stderr):
    +        try:
    +            stream.flush()
    +        except Exception:
    +            pass
    +    try:
    +        logging.shutdown()
    +    except Exception:
    +        pass
    +    if rc is None:
    +        exit_code = 0
    +    elif isinstance(rc, int):
    +        exit_code = rc
    +    else:
    +        exit_code = 1
    +    os._exit(exit_code)
    +
    +
    +_oneshot_cleanup_done = False
    +
    +
    +def _cleanup_oneshot_runtime() -> None:
    +    """Best-effort process-global cleanup before one-shot hard exit.
    +
    +    ``run_oneshot`` owns the agent-local cleanup (memory provider, agent.close,
    +    session_db.close — all in ``_run_agent``'s finally block). This mirrors the
    +    process-global pieces from ``cli.py:_run_cleanup()`` that would otherwise
    +    be skipped by ``os._exit``.
    +    """
    +    global _oneshot_cleanup_done
    +    if _oneshot_cleanup_done:
    +        return
    +    _oneshot_cleanup_done = True
    +    try:
    +        from tools.terminal_tool import cleanup_all_environments
    +        cleanup_all_environments()
    +    except Exception:
    +        pass
    +    try:
    +        from tools.async_delegation import interrupt_all
    +        interrupt_all(reason="oneshot shutdown")
    +    except Exception:
    +        pass
    +    try:
    +        from tools.browser_tool import _emergency_cleanup_all_sessions
    +        _emergency_cleanup_all_sessions()
    +    except Exception:
    +        pass
    +    try:
    +        from tools.mcp_tool import shutdown_mcp_servers
    +        shutdown_mcp_servers()
    +    except BaseException:
    +        pass
    +    try:
    +        from agent.auxiliary_client import shutdown_cached_clients
    +        shutdown_cached_clients()
    +    except Exception:
    +        pass
    +
    +
    +def _run_and_exit_oneshot(
    +    prompt: str,
    +    *,
    +    model: object = None,
    +    provider: object = None,
    +    toolsets: object = None,
    +    usage_file: object = None,
    +) -> None:
    +    try:
    +        from hermes_cli.oneshot import run_oneshot
    +
    +        rc = run_oneshot(
    +            prompt,
    +            model=model,
    +            provider=provider,
    +            toolsets=toolsets,
    +            usage_file=usage_file,
    +        )
    +    except KeyboardInterrupt:
    +        rc = 130
    +    except SystemExit as exc:
    +        if exc.code is not None and not isinstance(exc.code, int):
    +            print(exc.code, file=sys.stderr)
    +            rc = 1
    +        else:
    +            rc = exc.code
    +    except BaseException:
    +        # Defense-in-depth. ``run_oneshot`` already converts agent failures
    +        # into an int return code and only re-raises KeyboardInterrupt /
    +        # SystemExit (handled above). Anything still escaping here means
    +        # ``run_oneshot`` itself malfunctioned — surface it on stderr but never
    +        # fall through to normal interpreter teardown, which is the exact path
    +        # that aborts with SIGABRT on AL2023 (the bug this routine fixes).
    +        import traceback
    +        try:
    +            traceback.print_exc()
    +        except Exception:
    +            pass
    +        rc = 1
    +    try:
    +        _cleanup_oneshot_runtime()
    +    finally:
    +        # The hard exit is the safety boundary for #43055. Even an interrupt
    +        # during best-effort cleanup must not fall back into interpreter
    +        # finalization, where the reported native SIGABRT occurs.
    +        _exit_after_oneshot(rc)
    +
    +
     def _set_process_title() -> None:
         """Set the process title to 'hermes' so tools like 'ps', 'top', and
         'htop' show the app name instead of 'python3.xx'.
    ```

    ```diff
    @@ -12973,16 +13092,12 @@ def _try_termux_fast_cli_launch() -> bool:
     
         if getattr(args, "oneshot", None):
             _prepare_agent_startup(args)
    -        from hermes_cli.oneshot import run_oneshot
    -
    -        sys.exit(
    -            run_oneshot(
    -                args.oneshot,
    -                model=getattr(args, "model", None),
    -                provider=getattr(args, "provider", None),
    -                toolsets=getattr(args, "toolsets", None),
    -                usage_file=getattr(args, "usage_file", None),
    -            )
    +        _run_and_exit_oneshot(
    +            args.oneshot,
    +            model=getattr(args, "model", None),
    +            provider=getattr(args, "provider", None),
    +            toolsets=getattr(args, "toolsets", None),
    +            usage_file=getattr(args, "usage_file", None),
             )
     
         if (args.resume or args.continue_last) and args.command is None:
    ```

    ```diff
    @@ -15130,16 +15245,12 @@ def _export_one(session_id: str):
         # Handle top-level --oneshot / -z: single-shot mode, stdout = final
         # response only, nothing else. Bypasses cli.py entirely.
         if getattr(args, "oneshot", None):
    -        from hermes_cli.oneshot import run_oneshot
    -
    -        sys.exit(
    -            run_oneshot(
    -                args.oneshot,
    -                model=getattr(args, "model", None),
    -                provider=getattr(args, "provider", None),
    -                toolsets=getattr(args, "toolsets", None),
    -                usage_file=getattr(args, "usage_file", None),
    -            )
    +        _run_and_exit_oneshot(
    +            args.oneshot,
    +            model=getattr(args, "model", None),
    +            provider=getattr(args, "provider", None),
    +            toolsets=getattr(args, "toolsets", None),
    +            usage_file=getattr(args, "usage_file", None),
             )
     
         # Handle top-level --resume / --continue as shortcut to chat
    ```
  - `hermes_cli/oneshot.py`

    ```diff
    @@ -188,7 +188,7 @@ def run_oneshot(
                 run — even when the run fails — so pipelines can account for
                 spend per invocation.
     
    -    Returns the exit code.  Caller should sys.exit() with the return.
    +    Returns the exit code.  The caller owns process termination.
         """
         # Silence every stdlib logger for the duration.  AIAgent, tools, and
         # provider adapters all log to stderr through the root logger; file
    ```

    ```diff
    @@ -396,44 +396,76 @@ def _run_agent(
             toolsets_list = sorted(_get_platform_tools(cfg, "cli"))
     
         session_db = _create_session_db_for_oneshot()
    -    # Read the effective fallback chain from profile config so oneshot workers
    -    # honour the same merge semantics as interactive CLI and gateway sessions.
    -    _fb = get_fallback_chain(cfg)
    -
    -    agent = AIAgent(
    -        api_key=[redacted:secret-assignment]"api_key"),
    -        base_url=runtime.get("base_url"),
    -        provider=runtime.get("provider"),
    -        api_mode=runtime.get("api_mode"),
    -        model=effective_model,
    -        enabled_toolsets=toolsets_list,
    -        quiet_mode=True,
    -        platform="cli",
    -        session_db=session_db,
    -        credential_pool=[redacted:secret-assignment]"credential_pool"),
    -        fallback_model=_fb or None,
    -        # Interactive callbacks are intentionally NOT wired beyond this
    -        # one.  In oneshot mode there's no user sitting at a terminal:
    -        #   - clarify  → returns a synthetic "pick a default" instruction
    -        #                so the agent continues instead of stalling on
    -        #                the tool's built-in "not available" error
    -        #   - sudo password prompt → terminal_tool gates on
    -        #                HERMES_INTERACTIVE which we never set
    -        #   - shell-hook approval → auto-approved via HERMES_ACCEPT_HOOKS=1
    -        #                (set above); also falls back to deny on non-tty
    -        #   - dangerous-command approval → bypassed via HERMES_YOLO_MODE=1
    -        #   - skill secret capture → returns gracefully when no callback set
    -        clarify_callback=_oneshot_clarify_callback,
    -    )
    +    # The try spans agent construction (not just ``chat``) so the SQLite store
    +    # opened above is always closed — including when ``AIAgent(...)`` itself
    +    # raises on a provider/config error. The one-shot exit path hard-exits via
    +    # os._exit and skips finalizers, so an un-closed connection here would leak.
    +    agent = None
    +    try:
    +        # Read the effective fallback chain from profile config so oneshot
    +        # workers honour the same merge semantics as interactive CLI and
    +        # gateway sessions.
    +        _fb = get_fallback_chain(cfg)
    +
    +        agent = AIAgent(
    +            api_key=[redacted:secret-assignment]"api_key"),
    +            base_url=runtime.get("base_url"),
    +            provider=runtime.get("provider"),
    +            api_mode=runtime.get("api_mode"),
    +            model=effective_model,
    +            enabled_toolsets=toolsets_list,
    +            quiet_mode=True,
    +            platform="cli",
    +            session_db=session_db,
    +            credential_pool=[redacted:secret-assignment]"credential_pool"),
    +            fallback_model=_fb or None,
    +            # Interactive callbacks are intentionally NOT wired beyond this
    +            # one.  In oneshot mode there's no user sitting at a terminal:
    +            #   - clarify  → returns a synthetic "pick a default" instruction
    +            #                so the agent continues instead of stalling on
    +            #                the tool's built-in "not available" error
    +            #   - sudo password prompt → terminal_tool gates on
    +            #                HERMES_INTERACTIVE which we never set
    +            #   - shell-hook approval → auto-approved via HERMES_ACCEPT_HOOKS=1
    +            #                (set above); also falls back to deny on non-tty
    +            #   - dangerous-command approval → bypassed via HERMES_YOLO_MODE=1
    +            #   - skill secret capture → returns gracefully when no callback set
    +            clarify_callback=_oneshot_clarify_callback,
    +        )
     
    -    # Belt-and-braces: make sure AIAgent doesn't invoke any streaming
    -    # display callbacks that would bypass our stdout capture.
    -    agent.suppress_status_output = True
    -    agent.stream_delta_callback = None
    -    agent.tool_gen_callback = None
    +        # Belt-and-braces: make sure AIAgent doesn't invoke any streaming
    +        # display callbacks that would bypass our stdout capture.
    +        agent.suppress_status_output = True
    +        agent.stream_delta_callback = None
    +        agent.tool_gen_callback = None
     
    -    result = agent.run_conversation(prompt)
    -    return (result.get("final_response") or "", result)
    +        result = agent.run_conversation(prompt)
    +        return (result.get("final_response") or "", result)
    +    finally:
    +        # Ordering deliberately mirrors gateway/run.py:_cleanup_agent_resources,
    +        # NOT cli.py:_run_cleanup — oneshot has no _active_agent_ref and must
    +        # close the agent explicitly because the hard-exit path skips finalizers.
    +        if agent is not None:
    +            try:
    +                session_messages = getattr(agent, "_session_messages", None)
    +                if isinstance(session_messages, list):
    +                    agent.shutdown_memory_provider(session_messages)
    +                else:
    +                    agent.shutdown_memory_provider()
    +            except Exception:
    +                logging.debug("oneshot memory/context cleanup failed", exc_info=True)
    +            try:
    +                agent.close()
    +            except Exception:
    +                logging.debug("oneshot agent cleanup failed", exc_info=True)
    +        # agent.close() calls session_db.end_session() but leaves the connection
    +        # open; close it here to checkpoint the WAL before os._exit skips
    +        # finalizers.
    +        if session_db is not None:
    +            try:
    +                session_db.close()
    +            except Exception:
    +                logging.debug("oneshot session store cleanup failed", exc_info=True)
     
     
     def _oneshot_clarify_callback(question: str, choices=None) -> str:
    ```
  - `tests/hermes_cli/test_tui_resume_flow.py`

    ```diff
    @@ -1,7 +1,9 @@
     from argparse import Namespace
     import os
     from pathlib import Path
    +import subprocess
     import sys
    +import textwrap
     import types
     
     import pytest
    ```

    ```diff
    @@ -21,11 +23,17 @@ def _args(**overrides):
         return Namespace(**base)
     
     
    +def _raise_exit(rc):
    +    raise SystemExit(rc)
    +
    +
     @pytest.fixture
     def main_mod(monkeypatch):
         import hermes_cli.main as mod
     
         monkeypatch.setattr(mod, "_has_any_provider_configured", lambda: True)
    +    # Reset the idempotency guard so each test starts fresh.
    +    monkeypatch.setattr(mod, "_oneshot_cleanup_done", False)
         return mod
     
     
    ```

    ```diff
    @@ -353,7 +361,17 @@ def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod)
         monkeypatch.setattr(
             sys,
             "argv",
    -        ["hermes", "-z", "hello", "--model", "gpt-test", "--provider", "openai"],
    +        [
    +            "hermes",
    +            "-z",
    +            "hello",
    +            "--model",
    +            "gpt-test",
    +            "--provider",
    +            "openai",
    +            "--usage-file",
    +            "usage.json",
    +        ],
         )
         monkeypatch.setattr(
             main_mod, "_prepare_agent_startup", lambda args: prepared.append(args.command)
    ```

    ```diff
    @@ -368,6 +386,11 @@ def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod)
                 or 17
             ),
         )
    +    monkeypatch.setattr(
    +        main_mod,
    +        "_exit_after_oneshot",
    +        _raise_exit,
    +    )
     
         with pytest.raises(SystemExit) as exc:
             main_mod._try_termux_fast_cli_launch()
    ```

    ```diff
    @@ -379,7 +402,7 @@ def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod)
             "model": "gpt-test",
             "provider": "openai",
             "toolsets": None,
    -        "usage_file": None,
    +        "usage_file": "usage.json",
         }
     
     
    ```

    ```diff
    @@ -577,7 +600,17 @@ def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
         import hermes_cli.config as config_mod
     
         monkeypatch.setattr(
    -        sys, "argv", ["hermes", "-z", "hello", "--toolsets", "web,terminal"]
    +        sys,
    +        "argv",
    +        [
    +            "hermes",
    +            "-z",
    +            "hello",
    +            "--toolsets",
    +            "web,terminal",
    +            "--usage-file",
    +            "usage.json",
    +        ],
         )
         monkeypatch.setitem(
             sys.modules,
    ```

    ```diff
    @@ -608,6 +641,11 @@ def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
                 or 0
             ),
         )
    +    monkeypatch.setattr(
    +        main_mod,
    +        "_exit_after_oneshot",
    +        _raise_exit,
    +    )
     
         with pytest.raises(SystemExit) as exc:
             main_mod.main()
    ```

    ```diff
    @@ -618,10 +656,721 @@ def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
             "model": None,
             "provider": None,
             "toolsets": "web,terminal",
    -        "usage_file": None,
    +        "usage_file": "usage.json",
         }
     
     
    +def test_exit_after_oneshot_flushes_stdio_and_calls_os_exit(
    +    monkeypatch, main_mod
    +):
    +    flushed = []
    +    exits = []
    +
    +    class FakeStream:
    +        def __init__(self, name):
    +            self.name = name
    +
    +        def flush(self):
    +            flushed.append(self.name)
    +
    +    def fake_exit(rc):
    +        exits.append(rc)
    +        raise SystemExit(rc)
    +
    +    monkeypatch.setattr(main_mod.sys, "stdout", FakeStream("stdout"))
    +    monkeypatch.setattr(main_mod.sys, "stderr", FakeStream("stderr"))
    +    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    +    monkeypatch.setattr("logging.shutdown", lambda: None)
    +
    +    with pytest.raises(SystemExit) as exc:
    +        main_mod._exit_after_oneshot(17)
    +
    +    assert exc.value.code == 17
    +    assert exits == [17]
    +    assert flushed == ["stdout", "stderr"]
    +
    +
    +def test_exit_after_oneshot_invokes_logging_shutdown_in_order(
    +    monkeypatch, main_mod
    +):
    +    events = []
    +
    +    class FakeStream:
    +        def __init__(self, name):
    +            self.name = name
    +
    +        def flush(self):
    +            events.append(f"flush:{self.name}")
    +
    +    def fake_exit(rc):
    +        events.append(f"exit:{rc}")
    +        raise SystemExit(rc)
    +
    +    monkeypatch.setattr(main_mod.sys, "stdout", FakeStream("stdout"))
    +    monkeypatch.setattr(main_mod.sys, "stderr", FakeStream("stderr"))
    +    monkeypatch.setattr("logging.shutdown", lambda: events.append("shutdown"))
    +    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    +
    +    with pytest.raises(SystemExit) as exc:
    +        main_mod._exit_after_oneshot(0)
    +
    +    assert exc.value.code == 0
    +    assert events == ["flush:stdout", "flush:stderr", "shutdown", "exit:0"]
    +
    +
    +def test_exit_after_oneshot_exits_even_if_logging_shutdown_raises(
    +    monkeypatch, main_mod
    +):
    +    exits = []
    +
    +    def fake_exit(rc):
    +        exits.append(rc)
    +        raise SystemExit(rc)
    +
    +    monkeypatch.setattr(
    +        main_mod.sys, "stdout", types.SimpleNamespace(flush=lambda: None)
    +    )
    +    monkeypatch.setattr(
    +        main_mod.sys, "stderr", types.SimpleNamespace(flush=lambda: None)
    +    )
    +    monkeypatch.setattr(
    +        "logging.shutdown",
    +        lambda: (_ for _ in ()).throw(RuntimeError("shutdown failed")),
    +    )
    +    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    +
    +    with pytest.raises(SystemExit) as exc:
    +        main_mod._exit_after_oneshot(1)
    +
    +    assert exc.value.code == 1
    +    assert exits == [1]
    +
    +
    +def test_exit_after_oneshot_flushes_stderr_when_stdout_flush_fails(
    +    monkeypatch, main_mod
    +):
    +    flushed = []
    +    exits = []
    +
    +    class BadStdout:
    +        def flush(self):
    +            raise BrokenPipeError("pipe closed")
    +
    +    class FakeStderr:
    +        def flush(self):
    +            flushed.append("stderr")
    +
    +    def fake_exit(rc):
    +        exits.append(rc)
    +        raise SystemExit(rc)
    +
    +    monkeypatch.setattr(main_mod.sys, "stdout", BadStdout())
    +    monkeypatch.setattr(main_mod.sys, "stderr", FakeStderr())
    +    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    +    monkeypatch.setattr("logging.shutdown", lambda: None)
    +
    +    with pytest.raises(SystemExit) as exc:
    +        main_mod._exit_after_oneshot(2)
    +
    +    assert exc.value.code == 2
    +    assert exits == [2]
    +    assert flushed == ["stderr"]
    +
    +
    +def test_exit_after_oneshot_normalizes_non_int_exit_code(monkeypatch, main_mod):
    +    exits = []
    +
    +    def fake_exit(rc):
    +        exits.append(rc)
    +        raise SystemExit(rc)
    +
    +    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    +    monkeypatch.setattr("logging.shutdown", lambda: None)
    +
    +    with pytest.raises(SystemExit) as exc:
    +        main_mod._exit_after_oneshot(None)
    +
    +    assert exc.value.code == 0
    +    assert exits == [0]
    +
    +
    +def test_run_and_exit_oneshot_routes_system_exit_to_hard_exit(monkeypatch, main_mod):
    +    exits = []
    +
    +    def fake_run_oneshot(*_args, **_kwargs):
    +        raise SystemExit(2)
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    +    )
    +    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert exits == [2]
    +
    +
    +def test_run_and_exit_oneshot_routes_bare_system_exit_to_zero(monkeypatch, main_mod):
    +    exits = []
    +
    +    def fake_run_oneshot(*_args, **_kwargs):
    +        raise SystemExit
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    +    )
    +    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert exits == [None]
    +
    +
    +def test_run_and_exit_oneshot_prints_system_exit_message(
    +    monkeypatch, capsys, main_mod
    +):
    +    exits = []
    +
    +    def fake_run_oneshot(*_args, **_kwargs):
    +        raise SystemExit("fatal")
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    +    )
    +    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert exits == [1]
    +    captured = capsys.readouterr()
    +    assert captured.out == ""
    +    assert captured.err == "fatal\n"
    +
    +
    +def test_run_and_exit_oneshot_cleans_global_runtime_before_hard_exit(
    +    monkeypatch, main_mod
    +):
    +    events = []
    +
    +    def _mod(name, **attrs):
    +        fake = types.ModuleType(name)
    +        for key, value in attrs.items():
    +            setattr(fake, key, value)
    +        return fake
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: events.append("run") or 0),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.terminal_tool",
    +        _mod("tools.terminal_tool", cleanup_all_environments=lambda: events.append("terminal")),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.async_delegation",
    +        _mod("tools.async_delegation", interrupt_all=lambda **kw: events.append("delegation")),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.browser_tool",
    +        _mod("tools.browser_tool", _emergency_cleanup_all_sessions=lambda: events.append("browser")),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.mcp_tool",
    +        _mod("tools.mcp_tool", shutdown_mcp_servers=lambda: events.append("mcp")),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "agent.auxiliary_client",
    +        _mod("agent.auxiliary_client", shutdown_cached_clients=lambda: events.append("aux")),
    +    )
    +    monkeypatch.setattr(
    +        main_mod, "_exit_after_oneshot", lambda rc: events.append(f"exit:{rc}")
    +    )
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert events == ["run", "terminal", "delegation", "browser", "mcp", "aux", "exit:0"]
    +
    +
    +def test_run_and_exit_oneshot_still_exits_when_global_cleanup_raises(
    +    monkeypatch, main_mod
    +):
    +    events = []
    +
    +    def _raise_mcp():
    +        raise RuntimeError("mcp boom")
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.terminal_tool",
    +        types.SimpleNamespace(cleanup_all_environments=lambda: events.append("terminal")),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.async_delegation",
    +        types.SimpleNamespace(interrupt_all=lambda **kw: events.append("delegation")),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.browser_tool",
    +        types.SimpleNamespace(_emergency_cleanup_all_sessions=lambda: events.append("browser")),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "tools.mcp_tool",
    +        types.SimpleNamespace(shutdown_mcp_servers=_raise_mcp),
    +    )
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "agent.auxiliary_client",
    +        types.SimpleNamespace(shutdown_cached_clients=lambda: events.append("aux")),
    +    )
    +    monkeypatch.setattr(
    +        main_mod, "_exit_after_oneshot", lambda rc: events.append(f"exit:{rc}")
    +    )
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert events == ["terminal", "delegation", "browser", "aux", "exit:0"]
    +
    +
    +def test_run_and_exit_oneshot_hard_exits_when_cleanup_is_interrupted(
    +    monkeypatch, main_mod
    +):
    +    def _raise_keyboard_interrupt():
    +        raise KeyboardInterrupt
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0),
    +    )
    +    monkeypatch.setattr(
    +        main_mod, "_cleanup_oneshot_runtime", _raise_keyboard_interrupt
    +    )
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", _raise_exit)
    +
    +    with pytest.raises(SystemExit) as exc:
    +        main_mod._run_and_exit_oneshot("hello")
    +
    +    assert exc.value.code == 0
    +
    +
    +def test_run_and_exit_oneshot_routes_keyboard_interrupt_to_130(
    +    monkeypatch, main_mod
    +):
    +    exits = []
    +
    +    def fake_run_oneshot(*_args, **_kwargs):
    +        raise KeyboardInterrupt
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    +    )
    +    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert exits == [130]
    +
    +
    +def test_run_and_exit_oneshot_hard_exits_on_unexpected_exception(
    +    monkeypatch, main_mod, capsys
    +):
    +    # ``run_oneshot`` is contracted to convert agent failures into an int and
    +    # only re-raise KeyboardInterrupt / SystemExit. If it ever malfunctions and
    +    # lets another exception escape, the one-shot path must still hard-exit
    +    # (rc 1) rather than fall through to interpreter teardown — the exact path
    +    # that SIGABRTs on AL2023.
    +    exits = []
    +    cleaned = []
    +
    +    def fake_run_oneshot(*_args, **_kwargs):
    +        raise RuntimeError("run_oneshot itself blew up")
    +
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    +    )
    +    monkeypatch.setattr(
    +        main_mod, "_cleanup_oneshot_runtime", lambda: cleaned.append(True)
    +    )
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert exits == [1]
    +    # Global cleanup still runs on the defensive path — resources must not leak
    +    # just because run_oneshot malfunctioned.
    +    assert cleaned == [True]
    +    # The failure is surfaced on stderr, never swallowed silently.
    +    assert "run_oneshot itself blew up" in capsys.readouterr().err
    +
    +
    +def test_run_and_exit_oneshot_hard_exits_when_oneshot_import_fails(
    +    monkeypatch, main_mod, capsys
    +):
    +    import builtins
    +
    +    exits = []
    +    cleaned = []
    +    real_import = builtins.__import__
    +
    +    def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
    +        if name == "hermes_cli.oneshot" and "run_oneshot" in (fromlist or ()):
    +            raise RuntimeError("oneshot import blew up")
    +        return real_import(name, globals, locals, fromlist, level)
    +
    +    monkeypatch.setattr(builtins, "__import__", fake_import)
    +    monkeypatch.setattr(
    +        main_mod, "_cleanup_oneshot_runtime", lambda: cleaned.append(True)
    +    )
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))
    +
    +    main_mod._run_and_exit_oneshot("hello")
    +
    +    assert exits == [1]
    +    assert cleaned == [True]
    +    assert "oneshot import blew up" in capsys.readouterr().err
    +
    +
    +def test_oneshot_subprocess_exits_without_teardown_abort():
    +    program = textwrap.dedent(
    +        """
    +        import hermes_cli.oneshot as oneshot
    +        from hermes_cli.main import _exit_after_oneshot
    +
    +        oneshot._run_agent = lambda *args, **kwargs: ("ok", {"final_response": "ok"})
    +        _exit_after_oneshot(oneshot.run_oneshot("hello"))
    +        """
    +    )
    +
    +    result = subprocess.run(
    +        [sys.executable, "-c", program],
    +        cwd=Path(__file__).resolve().parents[2],
    +        capture_output=True,
    +        timeout=10,
    +        check=False,
    +    )
    +
    +    assert result.returncode == 0
    +    assert result.stdout == b"ok\n"
    +    # Don't demand byte-empty stderr — an import-time warning from the heavy
    +    # CLI import chain shouldn't fail this. What matters is no crash traceback.
    +    assert b"Traceback" not in result.stderr
    +
    +
    +def test_exit_after_oneshot_bypasses_late_atexit_abort():
    +    program = textwrap.dedent(
    +        """
    +        import atexit
    +        import os
    +        import sys
    +        from hermes_cli.main import _exit_after_oneshot
    +
    +        atexit.register(os.abort)
    +        sys.stdout.write("done\\n")
    +        _exit_after_oneshot(0)
    +        """
    +    )
    +
    +    result = subprocess.run(
    +        [sys.executable, "-c", program],
    +        cwd=Path(__file__).resolve().parents[2],
    +        capture_output=True,
    +        timeout=10,
    +        check=False,
    +    )
    +
    +    assert result.returncode == 0
    +    assert result.stdout == b"done\n"
    +
    +
    +def test_run_and_exit_oneshot_passes_through_nonzero_return(monkeypatch, main_mod):
    +    # A non-zero rc from run_oneshot (e.g. provider-without-model → 2, or the
    +    # empty-response guard → 1) must reach os._exit unchanged.
    +    exits = []
    +    monkeypatch.setitem(
    +        sys.modules,
    +        "hermes_cli.oneshot",
    +        types.SimpleNamespace(run_oneshot=lambda *a, **k: 2),
    +    )
    +    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    +    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))
    +
    +    main_mod._run_and_exit_oneshot("hi")
    +
    +    assert exits == [2]
    +
    +
    +def test_main_oneshot_path_bypasses_late_atexit_abort():
    +    # End-to-end through the real top-level ``main()`` ``-z`` path: a valid
    +    # response prints, then a late atexit handler that would abort is bypassed
    +    # by the hard exit, so the process reports success (#43055).
    +    program = textwrap.dedent(
    +        """
    +        import atexit
    +        import os
    +        import sys
    +        import types
    +
    +        import hermes_cli.main as main_mod
    +
    +        sys.argv = ["hermes", "-z", "hello"]
    +        main_mod._prepare_agent_startup = lambda args: None
    +
    +        def _fake_run_oneshot(prompt, **kwargs):
    +            print("ok")
    +            return 0
    +
    +        sys.modules["hermes_cli.oneshot"] = types.SimpleNamespace(
    +            run_oneshot=_fake_run_oneshot
    +        )
    +        atexit.register(os.abort)
    +        main_mod.main()
    +        """
    +    )
    +
    +    result = subprocess.run(
    +        [sys.executable, "-c", program],
    +        cwd=Path(__file__).resolve().parents[2],
    +        capture_output=True,
    +        timeout=30,
    +        check=False,
    +    )
    +
    +    assert result.returncode == 0
    +    assert result.stdout == b"ok\n"
    +    assert b"Traceback" not in result.stderr
    +
    +
    +def test_oneshot_run_agent_closes_agent_after_chat(monkeypatch):
    +    import hermes_cli.oneshot as oneshot_mod
    +
    +    closed = []
    +    shutdown_messages = []
    +
    +    class FakeAgent:
    +        def __init__(self, **_kwargs):
    +            self.suppress_status_output = False
    +            self.stream_delta_callback = object()
    +            self.tool_gen_callback = object()
    +            self._session_messages = [{"role": "user", "content": "hello"}]
    +
    +        def run_conversation(self, prompt, **_kwargs):
    +            assert prompt == "hello"
    +            return {"final_response": "done"}
    +
    +        def shutdown_memory_provider(self, messages=None):
    +            shutdown_messages.append(messages)
    +
    +        def close(self):
    +            closed.append(True)
    +
    +    monkeypatch.setitem(
    +        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.config.load_config",
    +        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.runtime_provider.resolve_runtime_provider",
    +        lambda **_kwargs: {
    +            "api_key": "key",
    +            "base_url": "https://example.invalid",
    +            "provider": "openai",
    +            "api_mode": "chat_completions",
    +            "credential_pool": None,
    +        },
    +    )
    +    monkeypatch.setattr(oneshot_mod, "_create_session_db_for_oneshot", lambda: None)
    +
    +    assert (
    +        oneshot_mod._run_agent(
    +            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
    +        )
    +        == ("done", {"final_response": "done"})
    +    )
    +    assert closed == [True]
    +    assert shutdown_messages == [[{"role": "user", "content": "hello"}]]
    +
    +
    +def test_oneshot_run_agent_closes_agent_when_chat_raises(monkeypatch):
    +    import hermes_cli.oneshot as oneshot_mod
    +
    +    closed = []
    +    shutdowns = []
    +
    +    class FakeAgent:
    +        def __init__(self, **_kwargs):
    +            self.suppress_status_output = False
    +            self.stream_delta_callback = object()
    +            self.tool_gen_callback = object()
    +
    +        def run_conversation(self, _prompt, **_kwargs):
    +            raise RuntimeError("boom")
    +
    +        def shutdown_memory_provider(self, messages=None):
    +            shutdowns.append(messages)
    +
    +        def close(self):
    +            closed.append(True)
    +
    +    monkeypatch.setitem(
    +        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.config.load_config",
    +        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.runtime_provider.resolve_runtime_provider",
    +        lambda **_kwargs: {
    +            "api_key": "key",
    +            "base_url": "https://example.invalid",
    +            "provider": "openai",
    +            "api_mode": "chat_completions",
    +            "credential_pool": None,
    +        },
    +    )
    +    monkeypatch.setattr(oneshot_mod, "_create_session_db_for_oneshot", lambda: None)
    +
    +    with pytest.raises(RuntimeError, match="boom"):
    +        oneshot_mod._run_agent(
    +            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
    +        )
    +    assert closed == [True]
    +    assert shutdowns == [None]
    +
    +
    +def test_oneshot_run_agent_closes_session_db(monkeypatch):
    +    # The one-shot exit path hard-exits via os._exit and skips finalizers, so
    +    # the recall SQLite store it opens must be closed explicitly (checkpointing
    +    # its WAL) rather than left to interpreter teardown.
    +    import hermes_cli.oneshot as oneshot_mod
    +
    +    db_closed = []
    +
    +    class FakeAgent:
    +        def __init__(self, **_kwargs):
    +            self.suppress_status_output = False
    +            self.stream_delta_callback = object()
    +            self.tool_gen_callback = object()
    +            self._session_messages = []
    +
    +        def run_conversation(self, _prompt, **_kwargs):
    +            return {"final_response": "done"}
    +
    +        def shutdown_memory_provider(self, messages=None):
    +            pass
    +
    +        def close(self):
    +            pass
    +
    +    class FakeSessionDB:
    +        def close(self):
    +            db_closed.append(True)
    +
    +    monkeypatch.setitem(
    +        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.config.load_config",
    +        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.runtime_provider.resolve_runtime_provider",
    +        lambda **_kwargs: {
    +            "api_key": "key",
    +            "base_url": "https://example.invalid",
    +            "provider": "openai",
    +            "api_mode": "chat_completions",
    +            "credential_pool": None,
    +        },
    +    )
    +    monkeypatch.setattr(
    +        oneshot_mod, "_create_session_db_for_oneshot", lambda: FakeSessionDB()
    +    )
    +
    +    assert (
    +        oneshot_mod._run_agent(
    +            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
    +        )
    +        == ("done", {"final_response": "done"})
    +    )
    +    assert db_closed == [True]
    +
    +
    +def test_oneshot_run_agent_closes_session_db_when_agent_init_raises(monkeypatch):
    +    # The recall store is opened before AIAgent is constructed. If construction
    +    # raises (bad provider/config/model), the store must still be closed — the
    +    # one-shot exit hard-exits via os._exit and skips finalizers, so an
    +    # un-closed connection would leave a stale WAL behind.
    +    import hermes_cli.oneshot as oneshot_mod
    +
    +    db_closed = []
    +
    +    class FakeSessionDB:
    +        def close(self):
    +            db_closed.append(True)
    +
    +    class FakeAgent:
    +        def __init__(self, **_kwargs):
    +            raise RuntimeError("init boom")
    +
    +    monkeypatch.setitem(
    +        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.config.load_config",
    +        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    +    )
    +    monkeypatch.setattr(
    +        "hermes_cli.runtime_provider.resolve_runtime_provider",
    +        lambda **_kwargs: {
    +            "api_key": "key",
    +            "base_url": "https://example.invalid",
    +            "provider": "openai",
    +            "api_mode": "chat_completions",
    +            "credential_pool": None,
    +        },
    +    )
    +    monkeypatch.setattr(
    +        oneshot_mod, "_create_session_db_for_oneshot", lambda: FakeSessionDB()
    +    )
    +
    +    with pytest.raises(RuntimeError, match="init boom"):
    +        oneshot_mod._run_agent(
    +            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
    +        )
    +
    +    assert db_closed == [True]
    +
    +
     def _stub_plugin_discovery(monkeypatch):
         monkeypatch.setitem(
             sys.modules,
    ```
- **#109328: Honcho gateway identity: wizard copy, 'peers map', sessionAiPeerPrefix (salvage #83981, #39130)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `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` (+21 more)
- **#11680: fix: cleanly shut down Honcho background workers**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `924bc67ee`
- **#33543: fix(cli): prevent SIGABRT on hermes -z process exit**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `244dabbd9`, `#67905`
- **#33701: fix(memory/honcho): align shutdown join timeout with Honcho HTTP timeout**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `476dbfed3a`, `#109328`
- **#37635: fix(oneshot): join memory daemon threads before process exit (#37632)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `bfa7a794cb2`, `244dabbd9`, `7e53b3ef82c`, `#67905`
- **#43698: fix(cli): avoid one-shot SIGABRT during teardown (#43055)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#67905`
- **#62550: fix(cli): shut down memory provider in oneshot mode to prevent SIGABRT**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#67905`, `#109328`, `#60616`
- **#67768: fix(cli): clean up oneshot resources before exit**. closed without merge
  - maintainer direction: category `volume_quota`; implication `active`

## Fix status

- Merged to main in PR #67905 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-09-13`.
- Subsystem: `agent`
