Skip to content

fix(gateway): stop macOS gateway EMFILE — raise launchd fd limit + fix Telegram pool leak - #39336

Open
gumclaw wants to merge 2 commits into
NousResearch:mainfrom
gumclaw:fix/launchd-gateway-fd-limit
Open

fix(gateway): stop macOS gateway EMFILE — raise launchd fd limit + fix Telegram pool leak#39336
gumclaw wants to merge 2 commits into
NousResearch:mainfrom
gumclaw:fix/launchd-gateway-fd-limit

Conversation

@gumclaw

@gumclaw gumclaw commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes [Errno 24] Too many open files (EMFILE) on the macOS launchd gateway, which breaks session writes (.tmp files in ~/.hermes/sessions/) and forces a /reset.

Two compounding root causes:

  1. launchd fd cap. Gateway launchd agents inherit macOS's default soft RLIMIT_NOFILE of 256. A long-lived gateway with a large ~/.hermes/sessions/ dir plus accumulated httpx transports exhausts it. The generated plist set no resource limit, and the plist is reconciled against the template on every start/restart/setup — so any manual edit was silently reverted to 256.
  2. Telegram fallback-transport pool leak. TelegramFallbackTransport builds its inner httpx.AsyncHTTPTransport instances with no limits, so they fall back to httpx defaults (max_connections=100, max_keepalive=20, keepalive_expiry=5s). PTB's connection_pool_size=512/pool_timeout only configure the outer httpx.AsyncClient, and httpx ignores the outer client's limits once a custom transport is supplied. On the fallback-IP path the effective pool was 100, producing Pool timeout: All connections in the connection pool are occupied send failures and hundreds of CLOSE_WAIT sockets that combine with the 256 cap to trigger EMFILE.

Related Issue

Refs #14210

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • launchd plist — Emit SoftResourceLimits/HardResourceLimitsNumberOfFiles (default 65536, overridable via HERMES_GATEWAY_MAX_FILES) in the generated plist, so the limit persists across template reconciliation.
  • TelegramFallbackTransport — Inject explicit httpx.Limits into every inner fallback transport (primary + each fallback): 512 conns / 512 keepalive / 20s expiry, overridable via HERMES_TELEGRAM_HTTP_POOL_SIZE and HERMES_TELEGRAM_HTTP_KEEPALIVE_EXPIRY. Caller-supplied limits are respected. The non-fallback path already honored connection_pool_size and is unaffected.

How to Test

Automated:

scripts/run_tests.sh tests/hermes_cli/test_gateway_service.py tests/gateway/test_telegram_network.py
  • tests/hermes_cli/test_gateway_service.py — launchd plist sets the fd limit, honors the HERMES_GATEWAY_MAX_FILES env override, and falls back to the default on invalid input.
  • tests/gateway/test_telegram_network.py — inner fallback transports get the large pool, the env override applies, an invalid env value falls back, and explicitly-supplied limits are respected.

Manual (reproduces the original EMFILE):

  1. On macOS, install/start the gateway as a launchd agent (hermes gateway install). The inherited soft cap is 256 (launchctl limit maxfiles), and under sustained session writes + Telegram fallback traffic the gateway logs [Errno 24] Too many open files.
  2. Apply this change and regenerate the plist (restart/setup).
  3. Verify SoftResourceLimits NumberOfFiles is 65536 in the generated plist; CLOSE_WAIT sockets stay flat (lsof -p <gateway_pid> | grep -c CLOSE_WAIT) and the EMFILE no longer occurs.

Result: targeted tests pass; ruff clean.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS (launchd fd limit + Telegram fallback pool)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (env-var overrides only)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — macOS-specific launchd fix; Telegram pool fix is platform-agnostic; pre-existing systemd/Linux gateway tests fail on macOS regardless of this change
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

For New Skills

N/A

Screenshots / Logs

Failing path (before):

OSError: [Errno 24] Too many open files writing session .tmp under ~/.hermes/sessions/, alongside Pool timeout: All connections in the connection pool are occupied on the Telegram fallback path.

Validation run:

scripts/run_tests.sh tests/hermes_cli/test_gateway_service.py tests/gateway/test_telegram_network.py

Passed.

gumclaw added 2 commits June 4, 2026 17:35
launchd agents inherit macOS's default soft open-file limit of 256.
A long-lived gateway exhausts this — a large ~/.hermes/sessions/ dir
plus accumulated async httpx transports hit EMFILE ('[Errno 24] Too
many open files') on session .tmp writes, breaking the session and
forcing a /reset. The kernel ceiling (kern.maxfilesperproc) is far
higher, so 256 was the only real constraint.

Add SoftResourceLimits/HardResourceLimits NumberOfFiles (default
65536, overridable via HERMES_GATEWAY_MAX_FILES) to the generated
launchd plist. Because the plist is reconciled against this template
on every gateway start/restart/setup, the limit now persists instead
of reverting to 256.

Refs NousResearch#14210
…OSE_WAIT leak

TelegramFallbackTransport builds its inner httpx.AsyncHTTPTransport
instances with no limits, so they silently use httpx's defaults
(max_connections=100, max_keepalive=20, keepalive_expiry=5s). PTB's
connection_pool_size=512 / pool_timeout settings only configure the
OUTER httpx.AsyncClient, and httpx ignores the outer client's limits
once a custom transport is supplied — so on the fallback-IP path the
effective pool was 100, not 512.

Under a long-lived gateway this manifests as:
  - 'Pool timeout: All connections in the connection pool are
    occupied. Request was *not* sent to Telegram.' send failures, and
  - hundreds of server-closed sockets stuck in CLOSE_WAIT (the 5s
    keepalive churns connections faster than they're reused),
which combine with the 256 launchd fd cap to trigger EMFILE.

Inject explicit httpx.Limits into every inner transport (primary +
each fallback), defaulting to 512 connections / 512 keepalive / 20s
expiry, overridable via HERMES_TELEGRAM_HTTP_POOL_SIZE and
HERMES_TELEGRAM_HTTP_KEEPALIVE_EXPIRY. Caller-supplied limits are
respected. The non-fallback path already honored connection_pool_size
via PTB's default transport and is unaffected.

Refs NousResearch#14210
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the detailed EMFILE investigation. The launchd portion remains relevant: current generate_launchd_plist() has no SoftResourceLimits/HardResourceLimits block (hermes_cli/gateway.py:3962-3969).

Problems

  • The Telegram fallback fix has already shipped through commit 01ee312de (plugins/platforms/telegram/adapter.py:3146-3164). It passes the tuned _pool_limits—including configured max_connections—directly into both TelegramFallbackTransport instances. Current regression coverage verifies forwarding to all inner transports (tests/gateway/test_telegram_network.py:357-386).
  • The added HERMES_GATEWAY_MAX_FILES and HERMES_TELEGRAM_HTTP_KEEPALIVE_EXPIRY knobs conflict with the non-secret configuration rule in AGENTS.md:102-106; behavioral settings should use config.yaml.

Suggested changes

  • Preserve only a launchd-specific resource-limit change, reworked for the current hermes_cli/gateway.py:3872 template and an approved configuration surface.
  • Drop the superseded Telegram portion rather than restoring its older implementation path.

Automated hermes-sweeper review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/telegram Telegram bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants