hermes chat -q 'msg' --resume SESSION_ID loaded the session history
but never passed it to run_conversation(), so the model responded
without prior context. The interactive mode already does this correctly.
Based on work by christopher-kapic in PR #2081. Fixes#2106.
* feat: add meme-generation skill
* Reduce meme skill prompt cost with tighter selection rules
* feat(skill): overhaul meme-generation into real image generator
Move from skills/creative/ to optional-skills/creative/ (niche skill,
not needed by default). Replace prompt-only meme concept brainstormer
with actual meme image generation:
- Python script using Pillow to overlay text on template images
- 10 curated templates with hand-tuned text positioning
- Dynamic access to ~100 popular imgflip templates via public API
- Custom image mode (--image): use AI-generated or any image as base
- Two text modes: overlay (white+outline on image) or bars (black bars)
- Vision verification workflow: use vision_analyze to QA the result
- Auto-scaling font with pixel-accurate word wrapping
- Template search via --search
- No API keys required
Original skill concept by adanaleycio (PR #1771), overhauled with
image generation and custom image support.
---------
Co-authored-by: adanaleycio <atillababa767@gmail.com>
Two fixes for local model context detection:
1. Hardcoded DEFAULT_CONTEXT_LENGTHS matching was case-sensitive.
'qwen' didn't match 'Qwen3.5-9B-Q4_K_M.gguf' because of the
capital Q. Now uses model.lower() for comparison.
2. Added compressor initialization logging showing the detected
context_length, threshold, model, provider, and base_url.
This makes turn-1 compression bugs diagnosable from logs —
previously there was no log of what context length was detected.
Reverts the s-enter and Kitty CSI keybindings from PR #2345/#2346.
The s-enter key notation causes 'Invalid key: s-enter' crash on
some prompt_toolkit versions, breaking hermes startup entirely.
PR #2346 was merged with unresolved git conflict markers (<<<<<<,
=======, >>>>>>>) in cli.py at line 6047, causing SyntaxError on
startup. Resolved by keeping both the Shift+Enter keybindings and
the tab handler.
Kitty-protocol terminals (Ghostty, WezTerm) encode Shift+Enter as
CSI 13;2u instead of plain Enter. Without this binding, raw escape
characters appear in the input buffer. Adds s-enter and the Kitty
escape sequence as newline-insert bindings.
Based on work by ygd58 in PR #1798. Fixes#1795.
Registry.py apostrophe sanitization change excluded (unrelated scope).
When the model produces malformed JSON in tool call arguments, the agent
loop was setting args={} and dispatching the tool anyway, wasting an
iteration and producing a confusing downstream error. Now the error is
returned directly as the tool result so the model can retry with valid JSON.
Co-authored-by: alireza78a <alireza78.crypto@gmail.com>
Streaming provides a better UX — tokens appear as they arrive instead
of waiting for the full response. show_reasoning remains false so
thinking blocks are not streamed to the user.
Fixes#1803. send_image_file, send_document, and send_video were missing
message_thread_id forwarding, causing them to fail in Telegram forum/supergroups
where thread_id is required. send_voice already handled this correctly. Adds
metadata parameter + message_thread_id to all three methods, and adds tests
covering the thread_id forwarding path.
PR #2314 checked for provider names 'alibaba-coding-plan' and
'alibaba-coding-plan-anthropic' which don't exist in the provider
registry. The provider is always 'alibaba' — the condition was dead
code. Fixed to check self.provider == 'alibaba'.
Based on PR #1749 by @erosika (reimplemented on current main).
Extracts three protected methods from run() so wrapper CLIs can extend
the TUI without overriding the entire method:
- _get_extra_tui_widgets(): inject widgets between spacer and status bar
- _register_extra_tui_keybindings(kb, input_area): add keybindings
- _build_tui_layout_children(**widgets): full control over ordering
Default implementations reproduce existing layout exactly. The inline
HSplit in run() now delegates to _build_tui_layout_children().
5 tests covering defaults, widget insertion position, and keybinding
registration.
When using Alibaba (DashScope) with an anthropic-compatible endpoint,
model names like qwen3.5-plus were being normalized to qwen3-5-plus.
Alibaba's API expects the dot. Added preserve_dots parameter to
normalize_model_name() and build_anthropic_kwargs().
Also fixed 401 auth: when provider is alibaba or base_url contains
dashscope/aliyuncs, use only the resolved API key (DASHSCOPE_API_KEY).
Never fall back to resolve_anthropic_token(), and skip Anthropic
credential refresh for DashScope endpoints.
Cherry-picked from PR #1748 by crazywriter1. Fixes#1739.
Users reported that the bot fails to resolve usernames without the
Server Members privileged intent enabled. Updated the setup docs
to mark it as Required instead of Optional.
Feedback from Blangs [MADD].
Bare strings like "image", "audio", "document" were appended to
media_types, but downstream run.py checks mtype.startswith("image/")
and mtype.startswith("audio/"), which never matched. This caused all
Mattermost file attachments to be silently dropped from vision/STT
processing. Use the actual MIME type from file_info instead.
Cherry-picked from PR #2319 by @itenev.
When the gateway fails to connect (e.g. PrivilegedIntentsRequired,
missing token), systemd's default RestartSec=10 with no start rate
limit causes rapid reconnect storms flooding logs and triggering
platform-side rate limits.
- StartLimitIntervalSec=600 + StartLimitBurst=5 in [Unit] (max 5
restarts per 10 min)
- RestartSec: 10 → 30
- Applied to both templates in gateway.py and scripts/hermes-gateway
The gateway config loader read config.yaml but never merged its
`platforms` key into the runtime config dict. This meant that
platform-specific settings defined under `platforms.<name>.extra`
(e.g. webhook routes) were silently ignored unless the user also
duplicated them in the legacy gateway.json file.
Merge `yaml_cfg["platforms"]` into `gw_data["platforms"]` with a
shallow deep-merge of the `extra` dict so that gateway.json defaults
are preserved while config.yaml values take precedence.
Closes#2305
Six improvements to reduce information loss during context compression,
informed by analysis of Cline, OpenCode, Pi-mono, Codex, and ClawdBot:
1. Structured summary template — sections for Goal, Progress (Done/
In Progress/Blocked), Key Decisions, Relevant Files, Next Steps,
and Critical Context. Forces the summarizer to preserve each
category instead of writing a vague paragraph.
2. Iterative summary updates — on re-compression, the prompt says
'PRESERVE existing info, ADD new progress, UPDATE done/in-progress
status.' Previous summary is stored and fed back to the summarizer
so accumulated context survives across multiple compactions.
3. Token-budget tail protection — instead of fixed protect_last_n=4,
walks backward keeping ~20K tokens of recent context. Adapts to
message density: sessions with big tool results protect fewer
messages, short exchanges protect more. Falls back to protect_last_n
for small conversations.
4. Tool output pruning (pre-pass) — before the expensive LLM summary,
replaces old tool result contents with a placeholder. This is free
(no LLM call) and can save 30%+ of context by itself.
5. Scaled summary budget — instead of fixed 2500 tokens, allocates 20%
of compressed content tokens (clamped to 2000-8000). A 50-turn
conversation gets more summary space than a 10-turn one.
6. Richer summarizer input — tool calls now include arguments (up to
500 chars) and tool results keep up to 3000 chars (was 1500).
The summarizer sees 'terminal(git status) → M src/config.py'
instead of just '[Tool calls: terminal]'.
When streaming is enabled, the base adapter receives None from
_handle_message (already_sent=True) and cannot run auto-TTS for
voice input. The runner was unconditionally skipping voice input
TTS assuming the base adapter would handle it.
Now the runner takes over TTS responsibility when streaming has
already delivered the text response, so voice channel playback
works with both streaming on and off.
Streaming off behavior is unchanged (default already_sent=False
preserves the original code path exactly).
Co-authored-by: 0xbyt4 <35742124+0xbyt4@users.noreply.github.com>
On macOS, zsh users may not have ~/.zshrc if they haven't customized
their shell yet. The installer would silently fail to add ~/.local/bin
to PATH, causing 'hermes: command not found' after installation.
- Check ~/.zprofile as fallback for zsh users (macOS login shell config)
- Create ~/.zshrc if neither config file exists
Cherry-picked from PR #2315 by erhnysr.
Co-authored-by: erhnysr <erhnysr@users.noreply.github.com>
Cherry-picked from PR #2290 by @Mibayy. Closes#2138.
When asyncio.run() raises RuntimeError (running loop exists), the
coroutine was created but never awaited, producing a RuntimeWarning
on GC. Extract coro before try, call coro.close() in the except
branch before falling back to ThreadPoolExecutor.
Cron deliveries were mirrored into the target gateway session as
assistant-role messages, causing consecutive assistant messages that
violate message alternation (issue #2221).
Instead of fixing the role, remove the mirror injection entirely.
Cron outputs already live in their own cron session and don't belong
in the interactive conversation history.
Delivered messages are now wrapped with a header (task name) and a
footer noting the agent cannot see or respond to the message, so
users have clear context about what they're reading.
Closes#2221
Cherry-picked from PR #2292 by @Mibayy. Closes#2134.
resolve_toolset() called visited.copy() per sibling include, breaking
dedup for diamond dependencies (D resolved twice via B and C paths)
and causing duplicate cycle warnings.
Fix: pass visited directly so siblings share the same set. The .copy()
for the all/* alias at the top level is kept so each top-level toolset
gets an independent pass. Removes the print() cycle warning since
hitting a visited name now usually means diamond (not a bug).