Skip to content

fix: dedup TTL expiry, compression provider fallback, TCP keepalive CLOSE-WAIT - #10405

Closed
d3momonkey wants to merge 3 commits into
NousResearch:mainfrom
d3momonkey:fix/dedup-ttl-compression-keepalive
Closed

fix: dedup TTL expiry, compression provider fallback, TCP keepalive CLOSE-WAIT#10405
d3momonkey wants to merge 3 commits into
NousResearch:mainfrom
d3momonkey:fix/dedup-ttl-compression-keepalive

Conversation

@d3momonkey

Copy link
Copy Markdown

Summary

Fixes three bugs identified in issues #10306, #10314, and #10324.


Fix 1 — #10306: MessageDeduplicator TTL never expires on low-traffic instances

Files: gateway/platforms/wecom.py, gateway/platforms/dingtalk.py

The _is_duplicate() method only pruned expired entries when the cache exceeded DEDUP_MAX_SIZE (1000). On low-traffic instances the cache stays small, so message IDs accumulated forever and were permanently treated as duplicates.

Fix: Check TTL on every lookup (not just size-overflow). The prune condition now fires when msg_id in self._seen_messages OR when over max size, so expired entries are evicted before the membership test.


Fix 2 — #10314: Context compression falls back to OpenRouter when summary_provider=auto

File: agent/auxiliary_client.py

When compression.summary_provider=auto (default) and summary_model is empty (default), _resolve_task_provider_model() returned ("auto", None, ...) which fed into _resolve_auto(). That chain picks OpenRouter first and uses google/gemini-3-flash-preview — a model that 404s on non-OpenRouter providers (DashScope, custom endpoints, etc.). This caused a 600s cooldown loop, making the agent appear frozen.

Fix: Added _read_main_provider() and wired it into _resolve_task_provider_model(): when task is compression and the result is still auto with no model, read the main configured provider/model directly and return it, skipping the OpenRouter fallback chain.


Fix 3 — #10324: Agent hangs indefinitely on CLOSE-WAIT when provider drops connection

File: run_agent.py

When a custom provider (e.g. LiteLLM proxy) drops a connection mid-stream, the socket enters TCP CLOSE-WAIT. Because httpx uses epoll_wait for async I/O and a CLOSE-WAIT socket with no buffered data does not become readable, the 900s timeout never starts and the process hangs forever requiring kill -9.

Fix: Add TCP keepalive socket options to the httpx transport in _create_openai_client():

  • SO_KEEPALIVE=1 — enable keepalives
  • TCP_KEEPIDLE=30 — start probes after 30s idle
  • TCP_KEEPINTVL=10 — probe every 10s
  • TCP_KEEPCNT=3 — give up after 3 failures

Worst-case detection window: ~60s instead of infinite. try/except fallback handles macOS/Windows where TCP_KEEPIDLE may be unavailable.


Testing

  • tests/gateway/ — all passing (pre-existing failures unchanged)
  • tests/tools/ — all passing (pre-existing failures unchanged)
  • No new test failures introduced

…LOSE-WAIT

Fixes NousResearch#10306 - MessageDeduplicator TTL never expires on low-traffic instances
- wecom.py + dingtalk.py: check TTL on every lookup, not just when over
  DEDUP_MAX_SIZE. Entries now expire after DEDUP_WINDOW_SECONDS regardless
  of cache size.

Fixes NousResearch#10314 - Context compression falls back to OpenRouter when summary_provider=auto
- auxiliary_client.py: add _read_main_provider() and use it in
  _resolve_task_provider_model() so compression uses the main configured
  provider instead of falling through to OpenRouter/gemini-3-flash-preview.
  Eliminates 404 errors and 600s cooldown loops for non-OpenRouter users.

Fixes NousResearch#10324 - Agent hangs indefinitely on CLOSE-WAIT when provider drops connection
- run_agent.py: add TCP keepalive socket options (SO_KEEPALIVE, TCP_KEEPIDLE=30s,
  TCP_KEEPINTVL=10s, TCP_KEEPCNT=3) to httpx transport in _create_openai_client().
  Dead connections from dropped LiteLLM/custom providers now detected within ~60s
  instead of hanging forever. try/except fallback for macOS/Windows.
Fixes NousResearch#10216 - Gateway --config flag crashes with JSONDecodeError
- gateway/run.py: replace json.load() with yaml.safe_load() in main()
  when loading config file passed via --config flag. json.load() was
  being used on a YAML file, immediately crashing on the first colon.

Fixes NousResearch#10234 - MCP tool wastes 3x tokens on Chinese/non-ASCII text
- tools/mcp_tool.py: add ensure_ascii=False to all 19 json.dumps()
  calls. Default ensure_ascii=True was escaping Chinese characters to
  \uXXXX sequences, inflating token counts by ~3-4x for CJK content.
@d3momonkey

Copy link
Copy Markdown
Author

Update: Added two more fixes to this branch:

Also investigated #10174 (on_memory_write bridge missing in sequential path) — the bridge described in the ticket does not exist anywhere in the codebase (neither in _invoke_tool nor anywhere else). Both paths are already symmetric. Closing that one as a false report.

…nt cache fd leak

Fixes NousResearch#10318 - hermes update --check not recognized
- hermes_cli/main.py: add --check flag to update_parser and implement
  check mode in cmd_update() — runs git fetch + rev-list to count
  commits behind, prints status without pulling/installing.

Fixes NousResearch#10313 - External skills get wrong skill_dir in _load_skill_payload
- tools/skills_tool.py: include absolute 'skill_dir' in skill_view() JSON
  response so callers don't have to reconstruct it.
- agent/skill_commands.py: use skill_dir from JSON response directly;
  fall back to SKILLS_DIR-relative path only for backward compat.

Fixes NousResearch#10225 - load_cli_config() clobbers gateway TERMINAL_CWD
- cli.py: check existing TERMINAL_CWD before resolving '.' to os.getcwd().
  Gateway's MESSAGING_CWD-derived value is now preserved.

Fixes NousResearch#10200 - AsyncOpenAI client cache leaks fds indefinitely
- agent/auxiliary_client.py: add MAX_CLIENT_CACHE_SIZE=32 cap with
  LRU-style eviction. Oldest clients are force-closed when cache grows
  beyond limit, preventing fd exhaustion in long-running gateways.
@d3momonkey

Copy link
Copy Markdown
Author

Update 2: Four more fixes added to the branch:

Running total: 9 bugs fixed across 9 files in this PR.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint platform/wecom WeCom / WeChat Work adapter platform/dingtalk DingTalk adapter labels Apr 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Fix 3 (TCP keepalive CLOSE-WAIT) overlaps with already-merged #10933 and #11277 which addressed #10324. Fix 1 (dedup TTL) overlaps with closed #10403. Verify which sub-fixes are still needed.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the thorough multi-fix PR, @d3momonkey! After an automated hermes-sweeper review against current main, all 9 fixes bundled here have already been independently merged.

Evidence by fix:

This is an automated hermes-sweeper review. Closing as implemented on main.

@teknium1 teknium1 closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/dingtalk DingTalk adapter platform/wecom WeCom / WeChat Work adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants