feat: add ollama as first-class provider for local model inference (#169) #170
Closed
Timmy
wants to merge 1 commits from
timmy/issue-169-ollama-provider into main
pull from: timmy/issue-169-ollama-provider
merge into: Timmy_Foundation:main
Timmy_Foundation:main
Timmy_Foundation:fix/kimi-fallback-rebase
Timmy_Foundation:bezalel/self-awareness-epic-203
Timmy_Foundation:fix/kimi-fallback-model
Timmy_Foundation:bezalel/pr-215-rescue
Timmy_Foundation:perplexity/mempalace-tests
Timmy_Foundation:upstream-sync
Timmy_Foundation:bezalel/fix-gitea-ci-runner-host-mode
Timmy_Foundation:claude/issue-192
Timmy_Foundation:claude/issue-190
Timmy_Foundation:bezalel/fix-indentation-error
Timmy_Foundation:bezalel/gitea-workflow-skill
Timmy_Foundation:rescue/ollama-provider
Timmy_Foundation:rescue/v011-obfuscation-fix
Timmy_Foundation:claw-code/issue-151
Timmy_Foundation:claw-code/issue-126
Timmy_Foundation:groq/issue-168
Timmy_Foundation:gemini/issue-24
Timmy_Foundation:bezalel/syntax-guard-ci
Timmy_Foundation:claude/issue-128
Timmy_Foundation:claude/issue-142
Timmy_Foundation:claude/issue-133
Timmy_Foundation:claude/issue-143
Timmy_Foundation:claude/issue-146
Timmy_Foundation:claude/issue-155
Timmy_Foundation:claude/issue-147
Timmy_Foundation:claude/issue-148
Timmy_Foundation:bezalel/notebook-workflow-demo
Timmy_Foundation:claude/issue-149
Timmy_Foundation:bezalel/forge-health-check
Timmy_Foundation:epic-999-phase-ii-forge
Timmy_Foundation:allegro/m1-stop-protocol
Timmy_Foundation:timmy/issue-123-process-resilience
Timmy_Foundation:timmy/issue-116-config-validation
Timmy_Foundation:epic-999-phase-i
Timmy_Foundation:security/v-011-skills-guard-bypass
Timmy_Foundation:gemini/security-hardening
Timmy_Foundation:gemini/sovereign-gitea-client
Timmy_Foundation:timmy-custom
Timmy_Foundation:security/fix-oauth-session-fixation
Timmy_Foundation:security/fix-skills-path-traversal
Timmy_Foundation:security/fix-file-toctou
Timmy_Foundation:security/fix-error-disclosure
Timmy_Foundation:security/add-rate-limiting
Timmy_Foundation:security/fix-browser-cdp
Timmy_Foundation:security/fix-docker-privilege
Timmy_Foundation:security/fix-auth-bypass
Timmy_Foundation:fix/sqlite-contention
Timmy_Foundation:tests/security-coverage
Timmy_Foundation:security/fix-race-condition
Timmy_Foundation:security/fix-ssrf
Timmy_Foundation:security/fix-secret-leakage
Timmy_Foundation:feat/gen-ai-evolution-phases-19-21
Timmy_Foundation:feat/gen-ai-evolution-phases-16-18
Timmy_Foundation:feat/gen-ai-evolution-phases-13-15
Timmy_Foundation:security/fix-path-traversal
Timmy_Foundation:security/fix-command-injection
Timmy_Foundation:feat/gen-ai-evolution-phases-10-12
Timmy_Foundation:feat/gen-ai-evolution-phases-7-9
Timmy_Foundation:feat/gen-ai-evolution-phases-4-6
Timmy_Foundation:feat/gen-ai-evolution-phases-1-3
Timmy_Foundation:feat/sovereign-evolution-redistribution
Timmy_Foundation:feat/apparatus-verification
Timmy_Foundation:feat/sovereign-intersymbolic-ai
Timmy_Foundation:feat/sovereign-learning-system
Timmy_Foundation:feat/sovereign-reasoning-engine
No Reviewers
Labels
Clear labels
CI
QA
assigned-claw-code
assigned-kimi
blocked
claw-code-done
claw-code-in-progress
duplicate
epic
gaming
infra
kimi-done
kimi-in-progress
mcp
morrowind
needs-review
p0-critical
p1-important
security
stale
velocity-engine
wont-fix
Continuous integration, runners, workflow issues
Quality assurance, testing, production audit
Queued for Code Claw (qwen/openrouter)
Task assigned to KimiClaw for processing
Blocked by external dependency or merge conflict
Code Claw completed this task
Code Claw is actively working
Duplicate of another issue
Epic - large feature with multiple sub-tasks
Gaming agent capabilities
Infrastructure, VPS, DNS, deployment
KimiClaw has completed this task
KimiClaw is actively working on this
MCP (Model Context Protocol) tools & servers
Morrowind Agent gameplay & MCP integration
PR or issue requires reviewer sign-off before merge/close
Security hardening, vulnerability fixes
No activity, pending triage or closure
Auto-generated by velocity engine
Closed as intentionally not fixing — explicit descope
Milestone
No items
No Milestone
Projects
Clear projects
No project
Assignees
Rockachopa
Timmy
allegro
antigravity
bezalel
claude
claw-code
codex-agent
ezra
gemini
google
grok
groq
hermes
kimi
manus
perplexity
sonnet
Clear assignees
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Timmy_Foundation/hermes-agent#170
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "timmy/issue-169-ollama-provider"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add
ollamaas a recognized inference provider so local models (Gemma4, Hermes3, Hermes4) can run through the agent harness without falling back to OpenRouter.This is the sovereignty blocker — local inference requires ollama to be a first-class provider, not aliased to
custom.Changes
PROVIDER_REGISTRYwithbase_url=http://localhost:11434/v1, dummy API key fallback (ollama needs no auth), removeollama→customalias inresolve_provider()ollamato--providerchoicesHermesOverlayfor ollama, removeollama→ollama-cloudaliasUsage
Ollama exposes an OpenAI-compatible API at
localhost:11434/v1. No API key required (dummyollamatoken used for credential checks). Override withOLLAMA_BASE_URLorOLLAMA_API_KEYenv vars.Testing
resolve_provider("ollama")returns"ollama"(not"custom")resolve_api_key_provider_credentials("ollama")returns{api_key: "ollama", base_url: "http://localhost:11434/v1"}normalize_provider("ollama")returns"ollama"provider_label("ollama")returns"Ollama (Local)"curated_models_for_provider("ollama")returns the model catalogCloses #169
OPENAI_BASE_URL was written to .env AND config.yaml, creating a dual-source confusion. Users (especially Docker) would see the URL in .env and assume that's where all config lives, then wonder why LLM_MODEL in .env didn't work. Changes: - Remove all 27 save_env_value("OPENAI_BASE_URL", ...) calls across main.py, setup.py, and tools_config.py - Remove OPENAI_BASE_URL env var reading from runtime_provider.py, cli.py, models.py, and gateway/run.py - Remove LLM_MODEL/HERMES_MODEL env var reading from gateway/run.py and auxiliary_client.py — config.yaml model.default is authoritative - Vision base URL now saved to config.yaml auxiliary.vision.base_url (both setup wizard and tools_config paths) - Tests updated to set config values instead of env vars Convention enforced: .env is for SECRETS only (API keys). All other configuration (model names, base URLs, provider selection) lives exclusively in config.yaml.The delivery target parser uses split(':', 1) which only splits on the first colon. For the documented format platform:chat_id:thread_id (e.g. 'telegram:-1001234567890:17585'), thread_id gets munged into chat_id and is never extracted. Fix: split(':', 2) to correctly extract all three parts. Also fix to_string() to include thread_id for proper round-tripping. The downstream plumbing in _deliver_to_platform() already handles thread_id correctly (line 292-293) — it just never received a value.* fix: root-level provider in config.yaml no longer overrides model.provider load_cli_config() had a priority inversion: a stale root-level 'provider' key in config.yaml would OVERRIDE the canonical 'model.provider' set by 'hermes model'. The gateway reads model.provider directly from YAML and worked correctly, but 'hermes chat -q' and the interactive CLI went through the merge logic and picked up the stale root-level key. Fix: root-level provider/base_url are now only used as a fallback when model.provider/model.base_url is not set (never as an override). Also added _normalize_root_model_keys() to config.py load_config() and save_config() — migrates root-level provider/base_url into the model section and removes the root-level keys permanently. Reported by (≧▽≦) in Discord: opencode-go provider persisted as a root-level key and overrode the correct model.provider=openrouter, causing 401 errors. * fix(security): redact secrets from execute_code sandbox output The execute_code sandbox stripped env vars with secret-like names from the child process (preventing os.environ access), but scripts could still read secrets from disk (e.g. open('~/.hermes/.env')) and print them to stdout. The raw values entered the model context unredacted. terminal_tool and file_tools already applied redact_sensitive_text() to their output — execute_code was the only tool that skipped this step. Now the same redaction runs on both stdout and stderr after ANSI stripping. Reported via Discord (not filed on GitHub to avoid public disclosure of the reproduction steps).When PyYAML is unavailable or YAML frontmatter is malformed, the fallback parser may return metadata as a string instead of a dict. This causes AttributeError when calling .get("hermes") on the string. Added explicit type checks to handle cases where metadata or hermes fields are not dicts, preventing the crash. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>OpenAI's newer models (GPT-5, Codex) give stronger instruction-following weight to the 'developer' role vs 'system'. Swap the role at the API boundary in _build_api_kwargs() for the chat_completions path so internal message representation stays consistent ('system' everywhere). Applies regardless of provider — OpenRouter, Nous portal, direct, etc. The codex_responses path (direct OpenAI) uses 'instructions' instead of message roles, so it's unaffected. DEVELOPER_ROLE_MODELS constant in prompt_builder.py defines the matching model name substrings: ('gpt-5', 'codex').By default, Hermes always threads replies to channel messages. Teams that prefer direct channel replies had no way to opt out without patching the source. Add a reply_in_thread option (default: true) to the Slack platform extra config: platforms: slack: extra: reply_in_thread: false When false, _resolve_thread_ts() returns None for top-level channel messages, so replies go directly to the channel. Messages already inside an existing thread are still replied in-thread to preserve conversation context. Default is true for full backward compatibility.ACP clients pass MCP server definitions in session/new, load_session, resume_session, and fork_session. Previously these were accepted but silently ignored — the agent never connected to them. This wires the mcp_servers parameter into the existing MCP registration pipeline (tools/mcp_tool.py) so client-provided servers are connected, their tools discovered, and the agent's tool surface refreshed before the first prompt. Changes: tools/mcp_tool.py: - Extract sanitize_mcp_name_component() to replace all non-[A-Za-z0-9_] characters (fixes crash when server names contain / or other chars that violate provider tool-name validation rules) - Use it in _convert_mcp_schema, _sync_mcp_toolsets, _build_utility_schemas - Extract register_mcp_servers(servers: dict) as a public API that takes an explicit {name: config} map. discover_mcp_tools() becomes a thin wrapper that loads config.yaml and calls register_mcp_servers() acp_adapter/server.py: - Add _register_session_mcp_servers() which converts ACP McpServerStdio / McpServerHttp / McpServerSse objects to Hermes MCP config dicts, registers them via asyncio.to_thread (avoids blocking the ACP event loop), then rebuilds agent.tools, valid_tool_names, and invalidates the cached system prompt - Call it from new_session, load_session, resume_session, fork_session Tested with Eden (theproxycompany.com) as ACP client — 5 MCP servers (HTTP + stdio) registered successfully, 110 tools available to the agent.- Detect if origin points to a fork (not NousResearch/hermes-agent) - Show warning when updating from a fork: origin URL - After pulling from origin/main on a fork: - Prompt to add upstream remote if not present - Respect ~/.hermes/.skip_upstream_prompt to avoid repeated prompts - Compare origin/main with upstream/main - If origin has commits not on upstream, skip (don't trample user's work) - If upstream is ahead, pull from upstream and try to sync fork - Use --force-with-lease for safe fork syncing Non-main branches are unaffected - they just pull from origin/{branch}. Co-authored-by: Avery <avery@hermes-agent.ai>When config.yaml has 'mcp_servers:' with no value, YAML parses it as None. dict.get('mcp_servers', {}) only returns the default when the key is absent, not when it's explicitly None. Use 'or {}' pattern to handle both cases, matching the other two assignment sites in the same file.Two fixes for Discord exec approval: 1. Register /approve and /deny as native Discord slash commands so they appear in Discord's command picker (autocomplete). Previously they were only handled as text commands, so users saw 'no commands found' when typing /approve. 2. Wire up the existing ExecApprovalView button UI (was dead code): - ExecApprovalView now calls resolve_gateway_approval() to actually unblock the waiting agent thread when a button is clicked - Gateway's _approval_notify_sync() detects adapters with send_exec_approval() and routes through the button UI - Added 'Allow Session' button for parity with /approve session - send_exec_approval() now accepts session_key and metadata for thread support - Graceful fallback to text-based /approve prompt if button send fails Also updates test mocks to include grey/secondary ButtonStyle and purple Color (used by new button styles).Three fixes for memory+profile isolation bugs: 1. memory_tool.py: Replace module-level MEMORY_DIR constant with get_memory_dir() function that calls get_hermes_home() dynamically. The old constant was cached at import time and could go stale if HERMES_HOME changed after import. Internal MemoryStore methods now call get_memory_dir() directly. MEMORY_DIR kept as backward-compat alias. 2. profiles.py: profile create --clone now copies MEMORY.md and USER.md from the source profile. These curated memory files are part of the agent's identity (same as SOUL.md) and should carry over on clone. 3. holographic plugin: initialize() now expands $HERMES_HOME and ${HERMES_HOME} in the db_path config value, so users can write 'db_path: $HERMES_HOME/memory_store.db' and it resolves to the active profile directory, not the default home. Tests updated to mock get_memory_dir() alongside the legacy MEMORY_DIR.Windows (CRLF) and old Mac (CR) line endings are normalised to LF before the 5-line collapse threshold is checked in handle_paste. Without this, markdown copied from Windows sources contains \r\n but the line counter (pasted_text.count('\n')) still works — however buf.insert_text() leaves bare \r characters in the buffer which some terminals render by moving the cursor to the start of the line, making multi-line pastes appear as a single overwritten line.Reads config.extra['group_topics'] to bind skills to specific thread_ids in supergroup/forum chats. Mirrors the dm_topics skill injection pattern but for group chat_type. Enables per-topic skill auto-loading in Falcon HQ. Config format: platforms.telegram.extra.group_topics: - chat_id: -1003853746818 topics: - name: FalconConnect thread_id: 5 skill: falconconnect-architectureThe config key skills.external_dirs and core resolution (get_all_skills_dirs, get_external_skills_dirs in agent/skill_utils.py) already existed but several code paths still only scanned SKILLS_DIR. Now external dirs are respected everywhere: - skills_categories(): scan all dirs for category discovery - _get_category_from_path(): resolve categories against any skills root - skill_manager_tool._find_skill(): search all dirs for edit/patch/delete - credential_files.get_skills_directory_mount(): mount all dirs into Docker/Singularity containers (external dirs at external_skills/<idx>) - credential_files.iter_skills_files(): list files from all dirs for Modal/Daytona upload - tools/environments/ssh.py: rsync all skill dirs to remote hosts - gateway _check_unavailable_skill(): check disabled skills across all dirs Usage in config.yaml: skills: external_dirs: - ~/repos/agent-skills/hermes - /shared/team-skillsTwo pre-existing issues causing test_file_read_guards timeouts on CI: 1. agent/redact.py: _ENV_ASSIGN_RE used unbounded [A-Z_]* with IGNORECASE, matching any letter/underscore to end-of-string at each position → O(n²) backtracking on 100K+ char inputs. Bounded to {0,50} since env var names are never that long. 2. tools/file_tools.py: redact_sensitive_text() ran BEFORE the character-count guard, so oversized content (that would be rejected anyway) went through the expensive regex first. Reordered to check size limit before redaction.Add MiniMax as a fifth TTS provider alongside Edge TTS, ElevenLabs, OpenAI, and NeuTTS. Supports speech-2.8-hd (recommended default) and speech-2.8-turbo models via the MiniMax T2A HTTP API. Changes: - Add _generate_minimax_tts() with hex-encoded audio decoding - Add MiniMax to provider dispatch, requirements check, and Telegram Opus compatibility handling - Add MiniMax to interactive setup wizard with API key prompt - Update TTS documentation and config example Configuration: tts: provider: "minimax" minimax: model: "speech-2.8-hd" voice_id: "English_Graceful_Lady" Requires MINIMAX_API_KEY environment variable. API reference: https://platform.minimax.io/docs/api-reference/speech-t2a-httpAdd docker_env option to terminal config — a dict of key-value pairs that get set inside Docker containers via -e flags at both container creation (docker run) and per-command execution (docker exec) time. This complements docker_forward_env (which reads values dynamically from the host process environment). docker_env is useful when Hermes runs as a systemd service without access to the user's shell environment — e.g. setting SSH_AUTH_SOCK or GNUPGHOME to known stable paths for SSH/GPG agent socket forwarding. Precedence: docker_env provides baseline values; docker_forward_env overrides for the same key. Config example: terminal: docker_env: SSH_AUTH_SOCK: /run/user/1000/ssh-agent.sock GNUPGHOME: /root/.gnupg docker_volumes: - /run/user/1000/ssh-agent.sock:/run/user/1000/ssh-agent.sock - /run/user/1000/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent- Browse: POST /api/v1/browse → GET /api/v1/fs/{ls,tree,stat} - Read: POST /api/v1/read[/abstract] → GET /api/v1/content/{read,abstract,overview} - System prompt: result.get('children') → len(result) (API returns list) - Content: result.get('content') → result is a plain string - Browse: result['entries'] → result is the list; is_dir → isDir (camelCase) - Browse: add rel_path and abstract fields to entry output Based on PR #4742 by catbusconductor. Auth header changes dropped (already on main via #4825).LLMs frequently return numbers as strings ("42" instead of 42) and booleans as strings ("true" instead of true). This causes silent failures with MCP tools and any tool with strictly-typed parameters. Added coerce_tool_args() in model_tools.py that runs before every tool dispatch. For each argument, it checks the tool registry schema and attempts safe coercion: - "42" → 42 when schema says "type": "integer" - "3.14" → 3.14 when schema says "type": "number" - "true"/"false" → True/False when schema says "type": "boolean" - Union types tried in order - Original values preserved when coercion fails or is not applicable Inspired by Block/goose tool argument coercion system.* feat: coerce tool call arguments to match JSON Schema types LLMs frequently return numbers as strings ("42" instead of 42) and booleans as strings ("true" instead of true). This causes silent failures with MCP tools and any tool with strictly-typed parameters. Added coerce_tool_args() in model_tools.py that runs before every tool dispatch. For each argument, it checks the tool registry schema and attempts safe coercion: - "42" → 42 when schema says "type": "integer" - "3.14" → 3.14 when schema says "type": "number" - "true"/"false" → True/False when schema says "type": "boolean" - Union types tried in order - Original values preserved when coercion fails or is not applicable Inspired by Block/goose tool argument coercion system. * fix: accept reasoning-only responses without retries — set content to "(empty)" Previously, when a model returned reasoning/thinking but no visible content, we entered a 120-line retry/classify/compress/salvage cascade that wasted 3+ API calls trying to "fix" the response. The model was done thinking — retrying with the same input just burned money. Now reasoning-only responses are accepted immediately: - Reasoning stays in the `reasoning` field (semantically correct) - Content set to "(empty)" — valid non-empty string every provider accepts - No retries, no compression triggers, no salvage logic - Session history contains "(empty)" not "" — prevents #2128 session poisoning where empty assistant content caused prefill rejections Removes ~120 lines, adds ~15. Saves 2-3 API calls per reasoning-only response. Fixes #2128.Add POST /v1/runs to start async agent runs and GET /v1/runs/{run_id}/events for SSE streaming of typed lifecycle events (tool.started, tool.completed, message.delta, reasoning.available, run.completed, run.failed). Changes the internal tool_progress_callback signature from positional (tool_name, preview, args) to event-type-first (event_type, tool_name, preview, args, **kwargs). Existing consumers filter on event_type and remain backward-compatible. Adds concurrency limit (_MAX_CONCURRENT_RUNS=10) and orphaned run sweep. Fixes logic inversion in cli.py _on_tool_progress where the original PR would have displayed internal tools instead of non-internal ones. Co-authored-by: Mibayy <mibayy@users.noreply.github.com>Add optional 'expression' parameter to browser_console that evaluates JavaScript in the page context (like DevTools console). Returns structured results with auto-JSON parsing. No new tool — extends the existing browser_console schema with ~20 tokens of overhead instead of adding a 12th browser tool. Both backends supported: - Browserbase: uses agent-browser 'eval' command via CDP - Camofox: uses /tabs/{tab_id}/eval endpoint with graceful degradation E2E verified: string eval, number eval, structured JSON, DOM manipulation, error handling, and original console-output mode all working.Implement tools/mcp_oauth.py — the OAuth adapter that mcp_tool.py's existing auth: oauth hook has been waiting for. Components: - HermesTokenStorage: persists tokens + client registration to HERMES_HOME/mcp-tokens/<server>.json with 0o600 permissions - Callback handler factory: per-flow isolated HTTP handlers (safe for concurrent OAuth flows across multiple MCP servers) - OAuthClientProvider integration: wraps the MCP SDK's httpx.Auth subclass which handles discovery, DCR, PKCE, token exchange, refresh, and step-up auth (403 insufficient_scope) automatically - Non-interactive detection: warns when gateway/cron environments try to OAuth without cached tokens - Pre-registered client support: injects client_id/secret from config for servers that don't support Dynamic Client Registration (e.g. Slack) - Path traversal protection on server names - remove_oauth_tokens() for cleanup Config format: mcp_servers: sentry: url: 'https://mcp.sentry.dev/mcp' auth: oauth oauth: # all optional client_id: '...' # skip DCR client_secret: '...' # confidential client scope: 'read write' # server-provided by default Also passes oauth config dict through from mcp_tool.py (was passing only server_name and url before). E2E verified: full OAuth flow (401 → discovery → DCR → authorize → token exchange → authenticated request → tokens persisted) against local test servers. 23 unit tests + 186 MCP suite tests pass.Cron agents were burning iterations trying to use send_message (which is disabled via messaging toolset) because their prompts said things like 'send the report to Telegram'. The scheduler handles delivery automatically via the deliver setting, but nothing told the agent that. Add a delivery guidance hint to _build_job_prompt alongside the existing [SILENT] hint: tells agents their final response is auto-delivered and they should NOT use send_message. Before: only [SILENT] suppression hint After: delivery guidance ('do NOT use send_message') + [SILENT] hintEnable Hermes tool execution through the copilot-acp adapter by: - Passing tool schemas and tool_choice into the ACP prompt text - Instructing ACP backend to emit <tool_call>{...}</tool_call> blocks - Parsing XML tool-call blocks and bare JSON fallback back into Hermes-compatible SimpleNamespace tool call objects - Setting finish_reason='tool_calls' when tool calls are extracted - Cleaning tool-call markup from response text Fix duplicate tool call extraction when both XML block and bare JSON regexes matched the same content (XML blocks now take precedence). Cherry-picked from PR #4536 by MestreY0d4-Uninter. Stripped heuristic fallback system (auto-synthesized tool calls from prose) and Portuguese-language patterns — tool execution should be model-decided, not heuristic-guessed.Centralize the skill → slash command registration that Telegram already had in commands.py so Discord uses the exact same priority system, filtering, and cap enforcement: 1. Core/built-in commands (never trimmed) 2. Plugin commands (never trimmed) 3. Skill commands (fill remaining slots, alphabetical, only tier trimmed) Changes: hermes_cli/commands.py: - Rename _TG_NAME_LIMIT → _CMD_NAME_LIMIT (32 chars shared by both platforms) - Rename _clamp_telegram_names → _clamp_command_names (generic) - Extract _collect_gateway_skill_entries() — shared plugin + skill collection with platform filtering, name sanitization, description truncation, and cap enforcement - Refactor telegram_menu_commands() to use the shared helper - Add discord_skill_commands() that returns (name, desc, cmd_key) triples - Preserve _sanitize_telegram_name() for Telegram-specific name cleaning gateway/platforms/discord.py: - Call discord_skill_commands() from _register_slash_commands() - Create app_commands.Command per skill entry with cmd_key callback - Respect 100-command global Discord limit - Log warning when skills are skipped due to cap Backward-compat aliases preserved for _TG_NAME_LIMIT and _clamp_telegram_names. Tests: 9 new tests (7 Discord + 2 backward-compat), 98 total pass. Inspired by PR #5498 (sprmn24). Closes #5480.launchctl kickstart returns exit code 113 ("Could not find service") when the plist exists but the job hasn't been bootstrapped into the runtime domain. The existing recovery path only caught exit code 3 ("unloaded"), causing an unhandled CalledProcessError. Exit code 113 means the same thing practically -- the service definition needs bootstrapping before it can be kicked. Add it to the same recovery path that already handles exit 3, matching the existing pattern in launchd_stop(). Follow-up: add a unit test covering the 113 recovery path.When an alias like 'claude' can't be resolved on the current provider, _resolve_alias_fallback() tries other providers. Previously it hardcoded ('openrouter', 'nous') — so '/model claude' on z.ai would resolve to openrouter even if the user doesn't have openrouter credentials but does have anthropic. Now the fallback uses the user's actual authenticated providers (detected via list_authenticated_providers which is backed by the models.dev in-memory cache). If no authenticated providers are found, falls back to the old ('openrouter', 'nous') for backwards compatibility. New helper: get_authenticated_provider_slugs() returns just the slug strings from list_authenticated_providers().Read requires_env from plugin.yaml after install and interactively prompt for any missing environment variables, saving them to ~/.hermes/.env. Supports two manifest formats: Simple (backwards-compatible): requires_env: - MY_API_KEY Rich (with metadata): requires_env: - name: MY_API_KEY description: "API key for Acme" url: "https://acme.com/keys" secret: true Already-set variables are skipped. Empty input skips gracefully. Secret values use getpass (hidden input). Ctrl+C aborts remaining prompts without error.- {__raw__} in webhook prompt templates dumps the full JSON payload (truncated at 4000 chars) - _deliver_cross_platform now passes thread_id/message_thread_id from deliver_extra as metadata, enabling Telegram forum topic delivery - Tests for both featuresPreviously the scheduler checked startswith('[SILENT]'), so agents that appended [SILENT] after an explanation (e.g. 'N items filtered.\n\n[SILENT]') would still trigger delivery. Change the check to 'in' so the marker is caught regardless of position. Add test_silent_trailing_suppresses_delivery to cover this case.Allow users to configure websocket reconnect behavior via platform extra config to reduce reconnect latency in production environments. The official Feishu SDK defaults to: - First reconnect: random jitter 0-30 seconds - Subsequent retries: 120 second intervals This can cause 20-30 second delays before reconnection after network interruptions. This commit makes these values configurable while keeping the SDK defaults for backward compatibility. Configuration via ~/.hermes/config.yaml: ```yaml platforms: feishu: extra: ws_reconnect_nonce: 0 # Disable first-reconnect jitter (default: 30) ws_reconnect_interval: 3 # Retry every 3 seconds (default: 120) ``` Invalid values (negative numbers, non-integers) fall back to SDK defaults. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>QA Review — @perplexity
Status: BLOCKED — needs rebase + conflict resolution
Issues Found
466 commits / 674 files changed — this branch has drifted massively from main. The actual ollama provider change touches ~4 files. The rest is accumulated main-branch history. This needs to be rebased and squashed down to the relevant commits only.
10 merge conflicts — gateway/platforms/api_server.py, gateway/run.py, gateway/stream_consumer.py, hermes_state.py, model_tools.py, pyproject.toml, run_agent.py, tests/gateway/test_stream_consumer.py, tools/mcp_oauth.py, tools/web_tools.py
All CI checks failing — Tests, Docs, Nix, Supply Chain Audit all red. Cannot merge.
No reviewers assigned — this PR has been sitting with zero review.
The Actual Change (Good)
The ollama provider addition itself looks correct and well-scoped: PROVIDER_REGISTRY entry, CLI choices, model catalog, HermesOverlay. This is the sovereignty blocker — it should land.
Recommendation
This is high priority — sovereignty depends on it. cc @Timmy
QA Review — Perplexity (Integration Architect)
Status: BLOCKED — unmergeable
This PR has drifted to 466 commits from main with 10 file conflicts. The actual ollama provider changes look solid in concept (sovereignty blocker, correct approach), but this PR is unreviewable in its current state.
Issues found:
Recommendation:
The feature itself is important for sovereignty. It just needs a clean delivery vehicle.
This PR has drifted 466 commits from
main(674 files changed) and is unmergeable.The actual ollama provider changes have been cherry-picked onto a fresh branch:
rescue/ollama-providerClosing this PR. Please review and merge #179 instead.
See Timmy_Foundation/the-nexus#916 for context.
Pull request closed