sync: merge main into dy-main - #21
Merged
Merged
Conversation
…llback
Add Lightpanda as an optional browser engine for local mode.
Lightpanda is a headless browser built from scratch in Zig -- faster
navigation than Chrome with significantly less memory.
One config line to enable:
browser:
engine: lightpanda
New functions in browser_tool.py:
- _get_browser_engine() -- config/env reader with validation + caching
- _should_inject_engine() -- only inject in local non-cloud mode
- _needs_lightpanda_fallback() -- detect empty/failed LP results
- _chrome_fallback_screenshot() -- temporary Chrome session for screenshots
- Engine injection in _run_browser_command (--engine flag)
- browser_vision pre-routes screenshots to Chrome when engine=lightpanda
Config:
- browser.engine in DEFAULT_CONFIG (auto/lightpanda/chrome)
- AGENT_BROWSER_ENGINE in OPTIONAL_ENV_VARS
- /browser status shows engine info in local mode
Rebased from PR NousResearch#7144 onto current main. All existing code preserved --
pure additions only (+520/-2).
25 new tests + 81 total browser tests pass (0 failures).
…mpt, MCP) The TUI SessionPanel banner now uses collapsible \u25b8/\u25be toggle sections matching the existing Chevron convention used for runtime agent details. Skills, system prompt, and MCP server lists are collapsed by default; tools remain expanded as the most actionable info. - tui_gateway/server.py: _session_info() now passes agent._cached_system_prompt through to the TUI frontend - ui-tui/src/types.ts: added system_prompt?: string to SessionInfo - ui-tui/src/components/branding.tsx: rewrote SessionPanel with CollapseToggle helper + per-section useState toggles Default states: tools=open, skills=collapsed, system=collapsed, mcp=collapsed. Clicking any \u25b8/\u25be header toggles that section.
System messages over 400 chars (system prompt, AGENTS.md, etc.) now render as a collapsed \u25b8/\u25be toggle line in the transcript, matching the Chevron convention used for runtime details. The summary shows the first line + char count; clicking expands to full content.
…esearch#20679) On Termux/Android aarch64 (and other platforms without prebuilt wheels for some optional extras), 'pip install -e .[all]' compiles C/Rust extensions from source. This can run for several minutes with zero network activity and — with --quiet — zero stdout. Users report 'hermes update hangs at Updating Python dependencies', Ctrl+C it, then re-run and see 'up to date' (because git pull already succeeded and the pip step was still working when they interrupted). Pip's default output is proportional to actual work (one line per Collecting / Building wheel for X / Installing), so removing --quiet costs nothing on fast hardware and prevents the false-hang interrupt loop on slow hardware. Reported via Discord on Termux/Android. Supersedes NousResearch#20466 which misdiagnosed the hang as PYTHONPATH shadowing (install.sh doesn't run during 'hermes update', and terminal() doesn't inherit PYTHONPATH).
…gression) (NousResearch#20673) CPython's logging module is not reentrant-safe. `Logger.isEnabledFor` caches level results in `Logger._cache`; under shutdown races the cache can be cleared (`Logger._clear_cache`, triggered by logging config changes from another thread) or mid-mutation when a signal fires, raising `KeyError: <level_int>` (e.g. `KeyError: 10` for DEBUG) inside the signal handler. When that happens, the KeyError escapes before the `raise KeyboardInterrupt()` on the next line can fire, which bypasses prompt_toolkit's normal interrupt unwind and surfaces as the EIO cascade originally reported in NousResearch#13710. Issue NousResearch#13710 shipped two defenses (asyncio exception handler + outer `except (KeyError, OSError)` with EIO suppression) that cover the EIO unwind path. This patch closes the remaining escape hatch: the `logger.debug` call at the top of `_signal_handler` itself. Wrap it in a bare `try/except Exception: pass` so logging can never raise through a signal handler. Observed in the wild: debug report on 0.12.0 (commit 8163d37) shows the exact stack — KeyError: 10 at logging/__init__.py:1742 inside the signal handler's `logger.debug`, followed by the EIO cascade from prompt_toolkit's emergency flush. Tests: adds `TestSignalHandlerLoggingRace` to `tests/hermes_cli/test_suppress_eio_on_interrupt.py` with 6 new cases: - normal path still raises KeyboardInterrupt - KeyError(10) from logger.debug does not escape - any Exception from logger.debug is swallowed - agent.interrupt still fires when logger.debug raises - agent.interrupt raising also does not escape - BaseException (SystemExit) is NOT swallowed — guard uses `except Exception` deliberately so real shutdown signals still propagate Closes NousResearch#13710 regression.
The verb-padding change dropped the leading space in durationSegment on
the assumption that the verb's trailing pad always supplies the gap. But
the unicode spinner style sets showVerb=false, making verbSegment an
empty string — in that mode the output would become `{frame}· {duration}`
with no separator. Add the space back; harmless when the verb segment
is shown (its trailing pad still provides the gap).
The Nous DS globals.css applies a global rule:
code { background: var(--midground); color: var(--background); }
This paints an opaque cream/yellow fill on every <code> element,
which hides text in the kanban drawer's event-payload, run-meta,
and worker-log panes (all rendered as <code>).
Fix: scope a reset inside .hermes-kanban so <code> elements inherit
their parent's color and stay transparent.
…usResearch#20702) Port Shop.app's upstream SKILL.md (https://shop.app/SKILL.md) into optional-skills/productivity/shop-app/ with Hermes-native adaptations: - Proper Hermes frontmatter (name, description<=60 chars, version, author, license, prerequisites, metadata.hermes tags + related_skills + homepage + upstream) - Swap Shop.app's bespoke 'message()' tool references for Hermes conventions: gateway adapters handle platform formatting, so the skill just writes markdown (no Telegram/WhatsApp/iMessage sections referencing a tool Hermes doesn't ship) - Name Hermes tools where relevant: curl via 'terminal', HTML policy pages via 'web_extract', try-on via 'image_generate' - Reframe session state as 'hold in your reasoning context for this conversation only' and forbid writing tokens to .env / disk — matches Hermes ephemeral-memory discipline - Drop NO_REPLY convention (Shop-app-runtime specific) - Trigger-first description so the skill loader picks it up when the user wants to search products, track orders, returns, or reorder
…uardrails (NousResearch#20709) Replaces the per-directory shadow-repo design with a single shared shadow git store at ~/.hermes/checkpoints/store/. Object DB is now deduplicated across every working directory the agent has ever touched; a dozen worktrees of the same project cost near-zero in additional disk. Why --- Pre-v2 design had three compounding problems that let ~/.hermes/checkpoints/ grow to multi-GB on active machines: 1. Each working directory got its own full shadow git repo — no object dedup across projects or across worktrees of the same project. 2. _prune() was a documented no-op: max_snapshots only limited the /rollback listing. Loose objects accumulated forever. 3. Defaults: enabled=True, auto_prune=False — users paid the disk cost without ever asking for /rollback. Field report on a single workstation: 847 MB across 47 shadow repos, mostly redundant clones of the hermes-agent source tree. Changes ------- - tools/checkpoint_manager.py: full rewrite. Single bare store, per-project refs (refs/hermes/<hash>), per-project indexes (store/indexes/<hash>), per-project metadata (store/projects/<hash>.json with workdir + created_at + last_touch). On first v2 init, any pre-v2 per-directory shadow repos are auto-migrated into legacy-<timestamp>/ so the new store starts clean. _prune() now actually rewrites the per-project ref to the last max_snapshots commits and runs git gc --prune=now. New _enforce_size_cap() drops oldest commits round-robin across projects when the store exceeds max_total_size_mb. _drop_oversize_from_index() filters any single file larger than max_file_size_mb out of the snapshot. - hermes_cli/checkpoints.py: new 'hermes checkpoints' CLI (status / list / prune / clear / clear-legacy) for managing the store outside a session. - hermes_cli/config.py: flipped defaults — enabled=False, max_snapshots=20, auto_prune=True. Added max_total_size_mb=500, max_file_size_mb=10. Tightened DEFAULT_EXCLUDES (added target/, *.so/*.dylib/*.dll, *.mp4/*.mov, *.zip/*.tar.gz, .worktrees/, .mypy_cache/, etc.). - run_agent.py / cli.py / gateway/run.py: thread the new kwargs through AIAgent and the startup auto_prune hooks. - Tests rewritten to match v2 storage while keeping backwards-compat coverage for the pre-v2 prune path (per-directory shadow repos under base/ are still swept correctly for anyone mid-migration). - Docs updated: user-guide/checkpoints-and-rollback.md explains the shared store, new defaults, migration, and the new CLI; reference/cli-commands.md documents 'hermes checkpoints'. E2E validated ------------- - Legacy migration: pre-v2 shadow repos auto-archived into legacy-<ts>/. - Object dedup: two projects with an identical shared.py blob resolve to 7 total objects in the store (v1 would have stored the blob twice). - max_snapshots=3 actually enforced: after 6 commits, list shows 3. - Orphan prune: deleting a project's workdir + 'hermes checkpoints prune --retention-days 0' removes its ref, index, and metadata; GC reclaims the objects. - max_file_size_mb=1 excludes a 2 MB weights.bin while keeping the tracked source code files. - hermes checkpoints {status,prune,clear,clear-legacy} all work from the CLI without an agent running. Breaking / migration -------------------- No in-place data migration — legacy per-directory shadow repos are moved into legacy-<timestamp>/ on first run. Old /rollback history is still accessible by inspecting the archive with git; run 'hermes checkpoints clear-legacy' to reclaim the space when ready. Users relying on /rollback must now set checkpoints.enabled=true (or pass --checkpoints) explicitly.
…OOLONG dropping long slash commands
When the user pastes a long slash command like \`/goal <long prose>\` into
\`hermes chat\`, the input flows into \`_detect_file_drop()\`, whose
\`starts_like_path\` prefilter accepts anything starting with \`/\` and
forwards it to \`_resolve_attachment_path()\`. That helper calls
\`Path.exists()\` which invokes \`os.stat()\`, which raises
\`OSError(errno=ENAMETOOLONG)\` — 63 on macOS, 36 on Linux — when the
candidate exceeds NAME_MAX (typically 255 bytes).
The OSError propagates up to the broad \`except Exception\` in
\`process_loop\` (cli.py:11798), gets logged at WARNING level, and the
user's input is silently dropped. From the user's POV the chat prompt
hangs — the only signal is in agent.log:
WARNING cli: process_loop unhandled error (msg may be lost):
[Errno 63] File name too long: "/goal Drive the space board..."
This affects any slash command with prose-length arguments — \`/goal\`
in particular but also \`/skill\`, \`/cron\`, custom user commands.
Fix: wrap the \`exists()\`/\`is_file()\` calls in try/except OSError so
structurally-invalid path candidates cleanly return None. The slash-
command dispatch path downstream (cli.py:11718) then handles the
input correctly.
Tests: two new regression cases in test_cli_file_drop.py cover the
original \`/goal\` reproducer and a synthetic long path. All 35 file-
drop tests pass.
Reproducer (without the fix):
python -c "from cli import _detect_file_drop;
_detect_file_drop('/goal ' + 'a'*300)"
→ OSError: [Errno 63] File name too long
Follow-up to the salvaged fix for /goal ENAMETOOLONG drop — adds AUTHOR_MAP entry so the release script resolves the commit author to the correct GitHub user.
…vices, pitfalls (NousResearch#20748) Replaces the 22-line stub with a ~320-line guide covering the parts of the Windows/WSL2 split that specifically affect Hermes users: - Why WSL2 (and not native Windows) - Install: distro choice, WSL1→2, systemd via /etc/wsl.conf - Filesystem boundary: /mnt/c vs \\wsl$, perf/perms/watchers/case, wslpath/wslview, CRLF + git core.autocrlf, clone-where guidance - Networking in both directions: - WSL → Windows services: links to the canonical WSL2 Networking section in integrations/providers.md (mirrored mode, NAT + host IP, bind addr, firewall) instead of duplicating - Windows/LAN → Hermes in WSL: mirrored vs NAT, netsh portproxy one-liner, firewall rule, webhook tunneling pointer - Long-running services: systemd gateway + Task Scheduler wsl.exe --exec 'sleep infinity' to keep the VM alive at login - GPU passthrough: NVIDIA works, AMD/Intel out of matrix - Common pitfalls: connection refused, /mnt/c slowness, CRLF ^M, UNC warnings, post-sleep clock drift, mirrored-mode DNS with VPN, PATH, Defender scanning, VHDX disk reclaim All internal links use site-absolute /docs/... form (matches the rest of user-guide/); all seven link targets verified to exist.
… map, opt-in fix (NousResearch#20749) * docs(providers): add model-provider-plugin authoring guide + fix stale refs New docs: - website/docs/developer-guide/model-provider-plugin.md — full authoring guide (directory layout, minimal example, ProviderProfile fields, overridable hooks, user overrides, api_mode selection, auth types, testing, pip distribution) - Wired into website/sidebars.ts under 'Extending' - Cross-references added in: - guides/build-a-hermes-plugin.md (tip block) - developer-guide/adding-providers.md - developer-guide/provider-runtime.md User guide: - user-guide/features/plugins.md: Plugin types table grows from 3 to 4 with 'Model providers' row Stale comment cleanup (providers/*.py → plugins/model-providers/<name>/): - hermes_cli/main.py:_is_profile_api_key_provider docstring - hermes_cli/doctor.py:_build_apikey_providers_list docstring - hermes_cli/auth.py: PROVIDER_REGISTRY + alias auto-extension comments - hermes_cli/models.py: CANONICAL_PROVIDERS auto-extension comment AGENTS.md: - Project-structure tree: added plugins/model-providers/ row - New section: 'Model-provider plugins' explaining discovery, override semantics, PluginManager integration, kind auto-coerce heuristic Verified: docusaurus build succeeds, new page renders, all 3 cross-links resolve. 347/347 targeted tests pass (tests/providers/, tests/hermes_cli/test_plugins.py, tests/hermes_cli/test_runtime_provider_resolution.py, tests/run_agent/test_provider_parity.py). * docs(plugins): add 'pluggable interfaces at a glance' maps to plugins.md + build-a-hermes-plugin Devs landing on either the user-guide plugin page or the build-a-plugin guide now get an upfront table of every distinct pluggable surface with a link to the right authoring doc. Previously they'd have to read the full general-plugin guide to discover that model providers / platforms / memory / context engines are separate systems. user-guide/features/plugins.md: - New 'Pluggable interfaces — where to go for each' section below the existing 4-kinds table - 10 rows covering every register_* surface (tool, hook, slash command, CLI subcommand, skill, model provider, platform, memory, context engine, image-gen) - Explicit note: TTS/STT are NOT plugin-extensible yet — documented with a pointer to the current config.yaml 'command providers' pattern and a note that register_tts_provider()/register_stt_provider() may come later guides/build-a-hermes-plugin.md: - New :::info 'Not sure which guide you need?' map at the top so devs see all pluggable interfaces before investing in this 737-line general-plugin walkthrough - Existing bottom :::tip expanded to include platform adapters alongside model/memory/context plugins Verified: - All 8 cross-doc links in the new plugins.md table resolve in a docusaurus build (SUCCESS, no new broken links) - TTS link corrected (features/voice → features/tts; latter exists) - Pre-existing broken links/anchors (cron-script-only, llms.txt, adding-platform-adapters#step-by-step-checklist) are unchanged * docs(plugins): correct TTS/STT pluggability \u2014 they ARE plugins (command-providers) Previous commit incorrectly said TTS/STT 'aren't plugin-extensible'. They are, via the config-driven command-provider pattern \u2014 any CLI that reads text and writes audio (or vice versa for STT) is automatically a plugin with zero Python. The tts.md docs cover this extensively and I missed it. plugins.md: - TTS row: 'Config-driven (not a Python plugin)', points at tts.md#custom-command-providers - STT row: points at tts.md#voice-message-transcription-stt (STT docs live in tts.md despite the filename) - Expanded note: TTS/STT use config-driven shell-command templates as their plugin surface (full tts.providers.<name> registry for TTS; HERMES_LOCAL_STT_COMMAND escape hatch for STT) - Any CLI that reads/writes files is automatically a plugin \u2014 no Python register_* API needed - Future register_tts_provider()/register_stt_provider() hooks mentioned as nice-to-have for SDK/streaming cases, not as the primary story build-a-hermes-plugin.md: - Same map update: TTS/STT rows explicit, footer note corrected Verified: - tts.md anchors (custom-command-providers, voice-message-transcription-stt) exist and resolve in docusaurus build (SUCCESS, no new broken links) * docs(plugins): expand pluggable interfaces table with MCP / event hooks / shell hooks / skill taps Broadened the scope beyond Python register_* hooks. Hermes has MULTIPLE plugin-style extension surfaces; they're now all in one table instead of being scattered across feature docs. Added rows for: - **MCP servers** — config.yaml mcp_servers.<name> auto-registers external tools from any MCP server. Huge extensibility surface, previously not linked from the plugin map. - **Gateway event hooks** — drop HOOK.yaml + handler.py into ~/.hermes/hooks/<name>/ to fire on gateway:startup, session:*, agent:*, command:* events. Separate from Python plugin hooks. - **Shell hooks** — hooks: block in config.yaml runs shell commands on events (notifications, auditing, etc.). - **Skill sources (taps)** — hermes skills tap add <repo> to pull in new skill registries beyond the built-in sources. Both docs updated: - user-guide/features/plugins.md: table column renamed to 'How' (mixes Python API + config-driven + drop-in-dir surfaces accurately) - guides/build-a-hermes-plugin.md: :::info map at top mirrors the new surfaces with a forward-link to the consolidated table Note block rewritten: instead of singling out TTS/STT as the 'different style' exception, now honestly describes that Hermes deliberately supports three plugin styles — Python APIs, config-driven commands, and drop-in manifest directories — and devs should pick the one that fits their integration. Not included (considered and rejected): - Transport layer (register_transport) — internal, not user-facing - Tool-call parsers — internal, VLLM phase-2 thing - Cloud browser providers — hardcoded registry, not drop-in yet - Terminal backends — hardcoded if/elif, not drop-in yet - Skill sources (the ABC) — hardcoded list, only taps are user-extensible Verified: - All 5 new anchors resolve (gateway-event-hooks, shell-hooks, skills-hub, custom-command-providers, voice-message-transcription-stt) - Docusaurus build SUCCESS, zero new broken links - Same 3 pre-existing broken links on main (cron-script-only, llms.txt, adding-platform-adapters#step-by-step-checklist) * docs(plugins): cover every pluggable surface in both the overview and how-to Both plugins.md and build-a-hermes-plugin.md now cover every extension surface end-to-end \u2014 general plugin APIs, specialized plugin types, config-driven surfaces \u2014 with concrete authoring patterns for each. plugins.md: - 'What plugins can do' table grows from 9 rows (general ctx.register_* only) to 14 rows covering register_platform, register_image_gen_provider, register_context_engine, MemoryProvider subclass, register_provider (model). Each row links to its full authoring guide. - New 'Plugin sub-categories' section under Plugin Discovery explains how plugins/platforms/, plugins/image_gen/, plugins/memory/, plugins/context_engine/, plugins/model-providers/ are routed to different loaders \u2014 PluginManager vs the per-category own-loader systems. - Explicit mention of user-override semantics at ~/.hermes/plugins/model-providers/ and ~/.hermes/plugins/memory/. build-a-hermes-plugin.md: - New '## Specialized plugin types' section (5 sub-sections): - Model provider plugins \u2014 ProviderProfile + plugin.yaml example, auto-wiring summary, link to full guide - Platform plugins \u2014 BasePlatformAdapter + register_platform() skeleton - Memory provider plugins \u2014 MemoryProvider subclass example - Context engine plugins \u2014 ContextEngine subclass example - Image-generation backends \u2014 ImageGenProvider + kind: backend example - New '## Non-Python extension surfaces' section (5 sub-sections): - MCP servers \u2014 config.yaml mcp_servers.<name> example - Gateway event hooks \u2014 HOOK.yaml + handler.py example - Shell hooks \u2014 hooks: block in config.yaml example - Skill sources (taps) \u2014 hermes skills tap add example - TTS / STT command templates \u2014 tts.providers.<name> with type: command - Distribute via pip / NixOS promoted from ### to ## (they were orphaned after the reorganization) Each specialized / non-Python section has a concrete, copy-pasteable example plus a 'Full guide:' link to the authoritative doc. Devs arriving at the build-a-hermes-plugin guide now see every extension surface at their disposal, not just the general tool/hook/slash-command surface. Verified: - Docusaurus build SUCCESS, zero new broken links - All new cross-links (developer-guide/model-provider-plugin, adding-platform-adapters, memory-provider-plugin, context-engine-plugin, user-guide/features/mcp, skills#skills-hub, hooks#gateway-event-hooks, hooks#shell-hooks, tts#custom-command-providers, tts#voice-message-transcription-stt) resolve - Same 3 pre-existing broken links on main (cron-script-only, llms.txt, adding-platform-adapters#step-by-step-checklist) * docs(plugins): fix opt-in inconsistency — not every plugin is gated The 'Every plugin is disabled by default' statement was wrong. Several plugin categories intentionally bypass plugins.enabled: - Bundled platform plugins (IRC, Teams) auto-load so shipped gateway channels are available out of the box. Activation per channel is via gateway.platforms.<name>.enabled. - Bundled backends (plugins/image_gen/*) auto-load so the default backend 'just works'. Selection via <category>.provider config. - Memory providers are all discovered; one is active via memory.provider. - Context engines are all discovered; one is active via context.engine. - Model providers: all 33 discovered at first get_provider_profile(); user picks via --provider / config. The plugins.enabled allow-list specifically gates: - Standalone plugins (general tools/hooks/slash commands) - User-installed backends - User-installed platforms (third-party gateway adapters) - Pip entry-point backends Which matches the actual code in hermes_cli/plugins.py:737 where the bundled+backend/platform check bypasses the allow-list. Rewrote '## Plugins are opt-in' to: - Retitle to 'Plugins are opt-in (with a few exceptions)' - Narrow opening claim to 'General plugins and user-installed backends are disabled by default' - Added 'What the allow-list does NOT gate' subsection with a full table of which bypass the gate and how they're activated instead - Fixed migration section wording (bundled platform/backend plugins never needed grandfathering) Verified: docusaurus build SUCCESS, zero new broken links.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Automated sync PR because
mainis ahead ofdy-main.This PR was opened automatically by
.github/workflows/auto-pr-main-to-dy-main.yml.