Auto-gen page slugs, catalog rows/paths, sidebar entries, and zh-Hans
mirrors follow the directory renames. Also updates the install path
official/creative/audiocraft -> official/creative/audiocraft-audio-generation
in the songwriting-and-ai-music pointer section.
Adds a device authorization grant flow alongside the existing loopback
OAuth flow, so `hermes setup` can connect to Honcho cloud from SSH and
other no-browser environments.
- oauth.py: new HTTP seams — _http_post_form_status (non-raising, since
RFC 8628 polling reads the OAuth error off a 400) and _http_get_json
for the RFC 8414 metadata probe
- oauth_flow.py: DeviceCode, request_device_code, poll_for_token with
slow_down backoff (+5s, capped at 60s) bounded by expires_in, typed
errors (AccessDenied, DeviceCodeExpired, AuthorizationTimeout), and
supports_device_login (fail-closed metadata gate); device flow ends in
the same install_grant tail as loopback so refresh/status work
unchanged
- oauth_flow.py: loopback callback now serves a "sign-in was not
completed" page on consent cancel instead of the success page
- cli.py: cloud menu offers oauth / device / apikey; the device option
only appears when the host advertises the grant, and becomes the
default when no browser is detected
- 18 new tests covering the full flow against a local fake AS, backoff
schedule, error mapping, deadline bound, metadata gate, and wizard
branches
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audited every reference against ground truth (COMMAND_REGISTRY, argparse
--help output, TOOLSETS dict, provider profiles, DEFAULT_CONFIG) and
corrected drift the restructure had carried over verbatim:
- slash-commands.md: rebuilt from the live registry. Removes the phantom
/skill command (never existed — skills load via /skills, hermes -s, or a
skill's own /<name> command) and the wrong /q alias on /quit (/q belongs
to /queue) — both reported by @liuhao1024 in #50608 and @gauravsaxena1997
in #50613. Adds ~25 real commands that were missing (/learn, /memory,
/pet, /hatch, /bundles, /moa, /suggestions, /blueprint, /whoami, …) with
correct aliases and CLI/GW scoping.
- cli-reference.md: verified flags (-z/--oneshot, --tui/--cli, --safe-mode),
real subcommand sets for config/setup/sessions/skills/gateway/mcp/profile/
auth/webhook/cron/skin/pets, added missing top-level commands (fallback,
project, moa, logs, console, hooks, security, backup); condensed tables.
- providers-and-models.md: rebuilt from the 36 shipped provider profiles
with their actual env vars (was 21 rows, several stale); folds in the
alias system and fallback-chain command.
- configuration.md: toolset table matches TOOLSETS (adds coding,
computer_use, video_gen, context_engine, project; drops nonexistent
messaging/rl), config sections match DEFAULT_CONFIG, STT/TTS provider
lists match the shipped set.
- background-systems.md: curator verbs match the argparse surface.
- troubleshooting/security-privacy/windows-quirks/contributor-guide: fixed
dangling 'section above/below' references from the body split; moved the
reset-permissions playbook to security-privacy.md (its routing home).
Reported-by: liuhao1024 <sunsky.lau@gmail.com>
Reported-by: gauravsaxena1997
Flips the default fan-out cadence from per_iteration (advisors re-run on
every tool iteration, multiplying advisor spend by tool-loop depth) to
user_turn (advisors run once on the first message of each user turn; the
acting aggregator works the rest of the tool loop with that turn's
advice). Until per-mode benchmarks justify a costlier default, MoA
defaults to the cheapest, lowest-impact cadence (#67199).
One default for everyone — no split legacy/new-preset semantics; presets
that want per-step advising set fanout: per_iteration explicitly. All
three modes (user_turn / per_iteration / every_n:N) remain selectable;
every_n:1 still collapses to per_iteration (semantic identity), while
unparseable values now fall to user_turn (the default).
Docs updated with a default-change note; the per-iteration rerun test
pins its mode explicitly.
Co-authored-by: skyer-flyyy <188930297+skyer-flyyy@users.noreply.github.com>
Every SKILL.md description over 60 chars was silently truncated to
57 chars + '...' in the system-prompt skill index
(extract_skill_description, agent/skill_utils.py), destroying the
routing signal for 69 of 179 skills — some descriptions ran to
1,005 chars.
Rewrites follow the authoring standard: <=60 chars, one sentence,
ends with a period, trigger front-loaded, no marketing words, no
skill-name repetition. Excess detail already lives in each skill's
body.
Includes the touchdesigner-mcp trim from PR #32361 (credit:
@JeliTron) and aligns with the router-precision direction of PR
#48780 (@John-Lussier). Docs catalogs + per-skill pages regenerated
via website/scripts/generate-skill-docs.py.
Co-authored-by: JeliTron <287797501+JeliTron@users.noreply.github.com>
Make the x_search / xurl boundary explicit in the skill, feature docs,
toolset metadata, setup note, and reference pages, while keeping the
model-facing x_search schema generic (no static xurl name).
Regression tests assert behavioral routing invariants rather than frozen
prose snapshots. Drop the stale CI-only plugin/hangup hunks already on
main so this rebases cleanly.
skills/computer-use/SKILL.md sat at the root of the bundled skills
tree as an uncategorized single-skill directory. Move it to
skills/autonomous-ai-agents/computer-use/ alongside claude-code,
codex, opencode, and hermes-agent.
- git mv skills/computer-use -> skills/autonomous-ai-agents/computer-use
(history preserved)
- root-level-skill docstring examples (commands.py,
generate-skill-docs.py) now use a generic placeholder instead of
naming a specific skill, since categorizing root-level skills is
ongoing
- docs: move the bundled page to autonomous-ai-agents-computer-use,
update sidebars.ts, skills-catalog.md, and the two skill-path
references in features/computer-use.md
E2E validated: fresh sync_skills() copies to the new nested path;
existing installs with the old flat copy keep it untouched (manifest
name-keyed, hash unchanged -> skipped, no duplicate);
_get_category_from_path resolves 'autonomous-ai-agents'; docusaurus
build green; 396 targeted tests pass.
Claude Code's /simplify was renamed away, community-restored, and rebuilt
since our 3-agent port (v2.1.63 -> v2.1.154+). This brings simplify-code
in line with the current upstream design, re-expressed in our own wording
and layered onto our existing risk-tier/confidence machinery:
- New Reviewer 4 (Altitude): flags band-aid fixes layered on shared
infrastructure — special cases, symptom patches with unfixed sibling
sites, workaround stacks — and points at the deeper mechanism fix.
Matches our AGENTS.md 'fix the class, not the site' rubric.
- Explicit cleanup-vs-bug-hunt boundary: this skill improves working
code; correctness review stays with requesting-code-review.
- Inline single-pass fallback when delegate_task is unavailable (leaf
subagents, delegation disabled) — previously the skill just broke;
now all four angles run sequentially with honest disclosure.
- Finding format gains a concrete-cost field.
- Efficiency reviewer adds closure-capture scope-retention leaks.
- Pitfalls: fan-out cap 3 -> 4, band-aid-vs-deliberate-boundary caveat,
no-bug-hunting drift guard.
Kept our value-adds upstream lacks: SAFE/CAREFUL/RISKY tiers,
Chesterton's Fence via git blame, dry-run/focus/scope modifiers.
The yuanbao skill is platform-specific guidance for Tencent Yuanbao
group chats — niche for the default bundled set. Per the
'when in doubt, optional' rule, ship it as an optional skill.
Install via: hermes skills install official/yuanbao/yuanbao
The nano-pdf description repeated the skill name ('via nano-pdf CLI'),
violating the skill authoring standard, and spent its char budget
restating the name instead of distinguishing the skill from the
structural pdf skill. New description: 'Edit text in existing PDFs via
natural-language prompts.' (56 chars) — no name repetition, and
'text in existing PDFs' contrasts cleanly with pdf's
'Create, merge, split, fill, and secure PDF files.'
Propagated to the auto-generated docs pages and zh-Hans locale
(catalog row + per-skill page) for locale parity.
Per the 'when in doubt, optional' rule — zero-shot image segmentation
via SAM is a niche computer-vision capability with heavy deps
(torch, transformers), not something most users load.
Install via: hermes skills install official/mlops/segment-anything
Both are heavy-dep, GPU-bound local music-generation skills (8-16GB VRAM,
torch stacks, manual source patches) that were bundled in two different
categories (media/ and mlops/models/). Per the 'when in doubt, optional'
rule they now sit side by side in optional-skills/creative/, and the
bundled songwriting-and-ai-music skill points at them for local generation.
- git mv skills/media/heartmula -> optional-skills/creative/heartmula
- git mv skills/mlops/models/audiocraft -> optional-skills/creative/audiocraft
- cross-link related_skills both ways + songwriting-and-ai-music
- new section 10 in songwriting-and-ai-music with install commands
- docs: bundled pages -> optional pages (en + zh-Hans), catalogs, sidebar
Install via:
hermes skills install official/creative/heartmula
hermes skills install official/creative/audiocraft
The hermes-agent skill body was a 51KB monolith loaded in full on every
trigger. It is now a lightweight hub (~12KB): identity, quick start, the
tmux orchestration guide (kept in-body — the autonomous-ai-agents category
contract), surface orientation, and hard invariants, with a routing table
into 18 reference files that carry the depth.
Absorbed four Hermes-specific skills as first-class references so their
content gains a discoverable home and room to grow without bloating any
primary body:
- skills/hermes-themes -> references/themes.md + templates/skin.yaml
- skills/hermes-desktop-plugins -> references/desktop-plugins.md + templates/plugin.js
- skills/productivity/tui-widgets -> references/tui-widgets.md + templates/clock.mjs
- skills/productivity/petdex -> references/petdex.md
New references extracted from the old body: cli-reference, slash-commands,
providers-and-models, configuration, project-context-files,
security-privacy, background-systems, windows-quirks, troubleshooting,
contributor-guide (native-mcp and webhooks already existed). Also commits
delegate-task-concurrency-diagnosis and portal-auth-for-third-party-apps,
which the old body referenced but the repo never shipped.
Description updated to cover the widened scope:
'Use, configure, theme, extend, and orchestrate Hermes Agent.' (60 chars)
skills/dogfood/SKILL.md sat at the root of the bundled skills tree,
making it one of the few uncategorized skills (Discord /skill
autocomplete listed it under 'uncategorized'; hermes_cli/commands.py
cited it as the example). Move it to
skills/software-development/dogfood/ alongside the other QA/testing
skills (test-driven-development, systematic-debugging,
requesting-code-review).
- git mv skills/dogfood -> skills/software-development/dogfood
(history preserved)
- fix test fixture path in tests/tools/test_browser_console.py
- update root-level-skill docstring examples (commands.py,
generate-skill-docs.py) to cite computer-use, which is still root-level
- drop 'dogfood' from the category list in hermes-agent-skill-authoring
SKILL.md (en + zh-Hans docs mirrors)
- docs: regenerate/move the bundled page to
software-development-dogfood (en + zh-Hans), update sidebars.ts,
skills-catalog.md, and the adversarial-ux-test related-skills link
E2E validated: fresh sync_skills() copies to the new nested path;
existing installs with the old flat copy keep it untouched (manifest is
name-keyed, hash unchanged -> skipped, no duplicate);
_get_category_from_path resolves 'software-development'; docusaurus
build green.
Maintainer scoping decision for the #51226 salvage: document that
select_context() is for engines that must REPLACE per-request context
(retrieval/routing) — pre_llm_call is inject-only by documented cache
design; that observation-only plugins should implement a MemoryProvider
(sync_turn) rather than a context engine, with on_turn_complete scoped
as the observation mirror for engines that already select; and that a
non-no-op select_context naturally changes the prompt-cache prefix on
turns where the selection changes — engines should return stable
selections when nothing changed.
- website guide: on_turn_complete() now carries the same best-effort coverage
caveat as the ABC docstring (fires from the finalization seam; abnormal
early-return paths bypass it) — removes the doc/code inconsistency.
- test: finalization seam emits on_turn_complete with usage=None + the
interrupted flag for an interrupted finalized turn. Docstring records that
the negative early-return-bypass half is best-effort and deferred to a
shared-seam follow-up rather than pinned via a full run_conversation harness.
- _apply_context_engine_selection: reject an empty list. all([]) is True, so
a [] returned by a failing/buggy engine previously replaced a valid request
with an empty message list the downstream sanitizers can't restore; now it
falls open to the unmodified request (honors the fail-open contract).
Thanks @johnnykor82 for catching this on #41918's review.
- test: empty list keeps the original request (fail-open regression).
- docs: document select_context()/on_turn_complete() in the public
context-engine plugin guide (were still describing only the old contract).
Routine automatic compression stays silent-by-design on chat platforms
(default unchanged, byte-identical). New opt-in config key
compression.progress_notices (bool, default false) opens a gate on the
gateway noise filter (_prepare_gateway_status_message) that lets ROUTINE
compression progress statuses through to chat surfaces.
- Membership is derived from the #69550 status template constants in
agent/conversation_compression.py (compiled to a literal-escaped regex,
never re-inlined wording), so unrelated noisy statuses (aux failures,
provider retry/rate-limit chatter) stay suppressed even when enabled.
- The compaction completion notice (COMPACTION_DONE_STATUS, #69546
lifecycle 'compacted' edge) already flows through the status path and
passes the filter — no new emit site needed.
- Config wired everywhere: hermes_cli/config.py DEFAULT_CONFIG,
cli-config.yaml.example, gateway raw-YAML read (live, mtime-cached),
gateway hot-reload cache-busting key list, website configuration docs.
- Failure notices and manual /compress feedback remain always-visible;
VISIBLE_COMPRESSION_MESSAGES and emit sites untouched.
Design by @havok-training (issue #52995).
Per the 'when in doubt, optional' rule — the live-kernel workflow needs
uv + JupyterLab + a running server + a cloned hamelnb repo, a niche
setup that shouldn't ship active by default.
Renamed jupyter-live-kernel -> jupyter-notebook (skill name, page slugs,
catalogs, sidebar, zh-Hans mirror, darwinian-evolver related_skills).
Install via: hermes skills install official/data-science/jupyter-notebook
Extends the fanout enum with 'every_n:<N>' (N >= 2): advisors run on the
first iteration of each user turn and every Nth tool iteration after it;
off-cadence iterations REUSE the cached guidance from the last on-cadence
run via the same cache mechanism the user_turn fanout uses, so the
aggregator still gets advice on every step. The cadence counter is scoped
per user turn (resets on a new user message) and only advances when the
advisory state actually changes, so streaming retries never consume a
cadence slot. Mapping form {mode: every_n, n: N} normalizes to the
canonical string. Unknown/degenerate values fall back to per_iteration.
Addresses issue #63393 (advisor fan-out multiplies turn latency/cost by
the tool-iteration count). Redesigned from PR #63448: the submitted shape
skipped references entirely on off-cadence iterations (aggregator ran
advice-less); this version keeps the last advice in play, credited for
the idea and cadence framing.
Config-gated, default-off (default fanout remains per_iteration).
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Follow-up fixes on top of the salvaged #22566 mechanism:
- N-collector now counts only REAL actionable user turns via
_is_actionable_user_turn + _is_synthetic_compression_user_turn —
the same filter pair _find_last_user_message_idx uses post-#69291.
The contributor's bare role=='user' + _is_context_summary_content
check let blank platform echoes and continuation/todo rows consume
N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
measured to change the tail cut on transcripts whose budget covers
only the last turn. min_tail_user_messages=1 delegates to the
existing single-user anchor; N>1 is opt-in, and the call site is
gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
N; tool-call/result pairs never split by the N-boundary (no-orphan
both directions); N-guarantee wins over tail_token_budget and the
_MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
parity pin; DEFAULT_CONFIG pin.
The phase-1 tool-result prune only runs inside compress(), which fires
near 50% of the context window, so it never triggers on large-window
models; old tool outputs then ride in history and are re-sent every turn.
Add prune_tool_results_only(): the same no-LLM prune on a separate, low
proactive_prune_tokens trigger, run as an elif to the compression branch.
Opt-in (default 0), protects the recent tail by message count.
Add the method to the ContextEngine base as a no-op default so pluggable
engines inherit it safely (the post-tool-call path never AttributeErrors on
a non-built-in engine); the built-in compressor supplies the real prune.
Register both keys under the top-level compression config with defaults and
document them.
Add --long-description / --long-description-file to `hermes slack
manifest` so the generated app manifest can carry Slack's
display_information.long_description (175–4,000 characters), with
validation of the length bounds, mutual-exclusion with --slashes-only,
and UTF-8 file input. Also propagate the manifest command's exit status
through cmd_slack so validation failures reach the shell.
Squash of the two commits from PR #65256 — one commit per contributor
on this salvage branch.
Salvaged from #65256
Build the full reaction pipeline on top of the #29916 base:
- Opt-in gate: slack.reaction_triggers (default OFF — reaction events
stay acked-and-dropped so busy channels don't wake the agent on every
emoji). 'true' routes reactions on the bot's OWN messages; an explicit
emoji-name list routes those emojis from any message (handoff flows).
- reaction_removed events now route too, distinguished by the
cross-platform text convention reaction:added:<emoji> /
reaction:removed:<emoji> (matches the Feishu and Photon adapters, so
agents and skills see one shape everywhere).
- Authorization: the reactor becomes the synthesized message's user, so
the early _is_user_authorized gate and allowed_channels whitelist
apply exactly as for typed messages. _hermes_force_process only skips
the mention requirement (a reaction on the bot's own message is
definitionally addressed to the bot), mirroring Feishu/Photon.
- Gateway hooks (#33111 by @johnkattenhorn): every human reaction on a
message item fires reaction:added / reaction:removed through the new
BasePlatformAdapter.set_reaction_handler → GatewayRunner
._handle_reaction_event → HookRegistry.emit, independent of the
routing opt-in. Documented in hooks.md.
- Channel handoff (#45265 by @Kev-fs): slack.reaction_trigger_target
routes the reaction turn to a configured channel (top-level via
_hermes_no_thread_response + reply-anchor suppression in
gateway/platforms/base.py) or C123:<ts> thread.
- Manifest: reaction_removed event subscription added alongside
reaction_added/reactions:read.
- Docs: slack.md Reaction Triggers section; hooks.md event table rows.
Also credits #44508 by @harrisonmedmedmetrics (inbound reaction_added
handling — same plumbing class, superseded by this consolidated shape).
Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com>
Co-authored-by: Kev-fs <kevin@fleetsmarts.net>
Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
Documents user-facing wave-1+2 Slack behavior that had no docs coverage:
- decision table for require_mention / free_response_channels /
require_mention_channels / thread_require_mention / strict_mention /
ignore_other_user_mentions and how they compose
- 'Accepting messages from other bots' section with the post-#69483
semantics: allow_bots=mentions requires a CURRENT mention from
peer bots (text or Block Kit blocks); thread state never admits them
- clarify one-tap buttons (choice buttons + Other free-text mode,
in-place resolution, double-click guard, expiry message)
- slash replies are ephemeral: replace-ack, chunking, 5-post cap with
explicit truncation notice, postEphemeral fallback, never-public rule
- cron deliver targeting (slack -> home channel, slack:C... channel,
slack:U... resolved to DM) incl. standalone sender + MEDIA uploads
- send_message media + bare-user-ID DM resolution and caption behavior
Refs #26184.
Salvaged from #45765 by @navahc09 — kept the PR's callout structure
and placement, rewrote the content to match current behavior:
Slack blocks native slash commands in threads and never delivers them,
so Hermes recognises a leading '!' as an alternate command prefix.
Post-C3 command fixes the bang form also works behind a mention
(@Hermes !cmd) and with leading whitespace; unknown '!' tokens pass
through to the agent unchanged. Cross-linked the detailed
slack.md section.
- Add SLACK_THREAD_REQUIRE_MENTION, SLACK_IGNORE_OTHER_USER_MENTIONS and
SLACK_REQUIRE_MENTION_CHANNELS to the conftest behavioral-env force-clear
list so config-loader side effects can't leak between tests (same class
of leak the existing SLACK_* entries guard against).
- Document thread_require_mention and require_mention_channels in the
Slack messaging guide next to the other mention-gating options.
Adds the option to the Mention & Trigger Behavior section: leading-
mention semantics, opt-in default, env var, and DM/MPIM scope (1:1 DMs
unaffected; MPIMs apply it like channels).
Claude-Session: https://claude.ai/code/session_01TKsNdptNdo9CqT2u7JMdkH
markVoicePlaybackInterrupted() / takeVoicePlaybackInterrupted() mirror
the backend latch in the renderer (the barge happens client-side, where
the audio plays). VAD barges and typing over playback mark it; the next
prompt.submit carries interrupted:true, which the TUI gateway latches
into the model note.
Removes Homebrew and PyPI wheel/sdist as Hermes distribution paths while
preserving the supported source, Docker, and Nix workflows.
Changes:
- Removes the Homebrew formula, PyPI publish workflow, sdist manifest
(MANIFEST.in), and wheel/sdist release-attachment logic from scripts/release.py.
- Keeps setuptools metadata and entry points required by editable installs
and Docker/Nix builds, but adds a setup.py guard that rejects wheel/sdist
builds outside a sealed Nix derivation (HERMES_NIX_BUILD=1).
- Removes pip/Homebrew install detection, PyPI update checks, the pip
self-update path, the deprecation-banner state, the postinstall subcommand,
wheel data-directory fallbacks in agent/i18n.py and hermes_constants.py,
and the ACP Registry manifest/version-lockstep release logic.
- Adds /nix/store/ path detection so `nix run` / `nix profile install`
installs (which don't set HERMES_MANAGED) are correctly identified as
"nix" rather than falling through to "git"/"unknown".
- Retired install-method values ("pip", "homebrew") in existing
.install_method stamps (both code-scoped and home-scoped) are ignored by
the allowlist reader and fall through to "unknown" instead of resurrecting
a retired enum value.
- Updates Nix packaging to ship bare runtime data (locales, optional-mcps)
through store symlinks and wrapper env vars instead of wheel data-files.
- Removes the ACP Registry manifest/icon and their version-lockstep tests.
- Deletes or rewrites packaging, pip-update, Homebrew, and ACP Registry
tests; adds parametrized coverage for the packaging build guard covering
BOTH sdist and wheel paths (the guards live in separate cmdclass entries
— a passing sdist test proves nothing about the wheel path).
- Updates installation/platform documentation and related user-facing copy.
- Adjusts the supply-chain scan so deleted install-hook files do not trigger
a finding, while additions or modifications still require the existing
ci-reviewed label gate.
Supported installation paths (unchanged):
- git installer (install.sh)
- Docker
- Nix/NixOS
- editable development installs (uv sync, uv pip install -e ., pip install -e .)
Update the CLI and messaging guides so the default `interrupt` mode reflects
the new behavior: a follow-up redirects the active turn (preserving displayed
reasoning and completed work, letting running tools finish at a safe boundary)
rather than hard-stopping it, with `/stop` still the explicit hard stop.
Follow-up for the salvaged #55800 idle-compaction commit:
- turn_context.py: treat a skipped _compress_context (per-session
compression lock held by another path, failure cooldown, anti-thrash
breaker, codex-native routing) as a strict no-op — only re-baseline
conversation_history and re-anchor current_turn_user_idx after a REAL
compaction. Also re-anchor the user-message index after idle compaction
(the PR predates the reanchor helper).
- hermes_cli/config.py: add idle_compact_after_seconds: 0 to
DEFAULT_CONFIG's compression block (the PR only had
cli-config.yaml.example).
- gateway/run.py: add the idle-compaction status wording to
_TELEGRAM_NOISY_STATUS_RE so the new 💤 message stays out of
human-facing chat surfaces (routine compaction is silent by design);
pin it in tests/gateway/test_telegram_noise_filter.py.
- docs: idle_compact_after_seconds in user-guide/configuration.md and
developer-guide/context-compression-and-caching.md parameter table.
- tests/agent/test_idle_compaction_lock_and_guards.py: end-to-end
coverage with a real AIAgent + SessionDB proving the idle path honors
the per-session compression lock (added after the PR), the persisted
failure cooldown, and the anti-thrash breaker, and that the lock is
released after an idle-triggered compaction.
Salvaged from #55800 by @iso2kx. Implements #27579.
Add compression.threshold_tokens config option that sets an absolute
token cap for auto-compaction. When configured alongside the existing
ratio-based threshold, the effective trigger point is the lower of the
two, so compression never fires later than the user's preferred token
count regardless of which model is active.
This solves the problem where switching between models with different
context windows (e.g. 1M → 400K) shifts the absolute trigger point,
causing premature or delayed compression.
Rework from PR #24279 addressing sweeper feedback:
- The cap is now a first-class compressor configuration value
(threshold_tokens_cap parameter on ContextCompressor.__init__),
not a post-construction patch on the live instance.
- Applied in both __init__ and update_model() so it survives model
switches and fallback activations (the old approach was undone by
update_model() restoring _configured_threshold_percent).
- Clamped to the model's context length so a cap above the window is
a no-op (ratio-based threshold wins).
- Works with max_tokens output-token reservations.
- Added 9 tests covering cap-vs-ratio selection, model switch survival,
context-length clamping, max_tokens interaction, and invalid values.
- Updated user-facing configuration docs.
- Removed unrelated background-review/curator/Honcho changes (main
already contains background-review memory isolation in 973f27e95).
Config example:
compression:
threshold: 0.50
threshold_tokens: 200000 # never compress later than 200K tokens
Follow-up to the salvaged contributor commit, closing the three gaps
flagged in the sweeper review:
1. Init ordering: assign compression.model_thresholds to a selected
plugin context engine BEFORE the initial update_model() call in
agent_init.py, so the initial model's override applies from init
(previously it only took effect after the first /model switch).
Base-class ContextEngine.update_model() now snapshots the
pre-override percent once so repeated switches fall back to the
engine's configured threshold, not a previous model's override.
2. DEFAULT_CONFIG: add compression.model_thresholds (empty map) to
hermes_cli/config.py — additive key, no _config_version bump.
3. Docs: document the key in
website/docs/developer-guide/context-compression-and-caching.md
(yaml example, parameter table, dedicated section) and update the
plugin-boundary note in context-engine-plugin.md to state the
explicit context-engine contract for model_thresholds.
Adds tests/run_agent/test_per_model_threshold_init_ordering.py:
plugin-engine AIAgent init regression (override applies at init,
empty map unchanged), DEFAULT_CONFIG key presence, floor interaction
on the model-switch path (override below the small-context floor is
raised to the floor; above the floor wins), and base-class config
snapshot across repeated switches. Also maps @bennybuoy in
contributors/emails/.
Children inherit the parent's env, repo, and toolsets but were denied
execute_code ('children should reason step-by-step, not write scripts').
That forces subagents doing mechanical multi-step work (batch file
reads, fetch-N-pages loops, filter-before-context reductions) to burn
reasoning iterations one tool call at a time.
- Remove execute_code from DELEGATE_BLOCKED_TOOLS
- Stop stripping the code_execution toolset from child bundles
- No recursion risk: the sandbox bridges only the 7 SANDBOX_ALLOWED_TOOLS
(web/file/terminal) — delegate_task and execute_code itself are not
reachable from inside a sandbox script
- Update schema text, AGENTS.md, and delegation docs
- Tests: blocked-constant, strip, and child-assembly tests updated;
new test pins execute_code as intentionally unblocked
The Desktop app can now sign in to a gated gateway using the user's SYSTEM
browser and OAuth 2.0 for Native Apps (RFC 8252) instead of an embedded
Electron BrowserWindow, and authenticates with bearer tokens it holds itself
instead of relying on HttpOnly browser session cookies.
Why brokered: the upstream IDP (Nous Portal) binds client_id to the gateway
instance and only permits redirect_uris on the gateway's own origin, so a
desktop loopback redirect can't be a direct Portal client. The gateway
therefore acts as the authorization server TO the desktop and an OAuth client
TO the Portal, reusing the existing PKCE start_login/complete_login provider
path unchanged.
Server (Ben's dashboard-auth lane):
- native_flow.py: in-memory broker — binds the desktop's PKCE challenge to a
completed Session, mints a single-use, short-TTL, PKCE-verified gateway
authorization code. Constant-time compare, single-use (consumed before the
PKCE check so a wrong verifier can't be retried), capacity-bounded.
- routes.py: GET /auth/native/authorize (starts the brokered PKCE login,
loopback-only redirect_uri, S256-only), POST /auth/native/token (loopback
code + verifier -> tokens in the JSON body, never Set-Cookie), POST
/auth/native/refresh (desktop-held RT rotation). /auth/callback branches to
mint a loopback code + 302 to 127.0.0.1 when a broker_state rides the PKCE
cookie; the cookie/SPA path is untouched.
- middleware.py: the gate accepts Authorization: Bearer <access_token>,
verified via the same verify_session provider stack (no cookie set/read),
with the same "provider unreachable -> 503, not logout" semantics.
- web_server.py /api/status: advertise auth_flows (["cookie","native_pkce"])
so clients can detect the capability; native_pkce only when a brokerable
OAuth provider is registered.
Desktop (Ben's lane):
- native-oauth.ts: pure PKCE/capability/URL/callback/token helpers.
- native-oauth-login.ts: loopback-listener orchestration (system browser via
openExternal, ephemeral 127.0.0.1 listener, state/PKCE verification), all
I/O injected for testability.
- main.ts: capability-gated oauth-login IPC — native flow when advertised,
automatic fallback to the existing embedded-webview cookie flow otherwise;
tokens stored encrypted (safeStorage/OS keychain), REST + ws-ticket
authenticated by bearer, transparent refresh, logout clears both shapes.
Tests: 18 server pytest (broker unit + full authorize->callback->token E2E +
cookieless bearer auth of a gated route + ws-ticket mint + capability
advertisement + refresh); desktop node --test/vitest for both pure modules
(PKCE, capability detection, callback CSRF, loopback round trip, timeout,
browser-open failure). Electron project typechecks clean.
Docs: website/docs/guides/desktop-native-signin.md.
Reworks the salvaged command module into a CommandSource(SecretSource)
registered as the third bundled source, composing with Bitwarden and
1Password through the apply_all() orchestrator — enable any combination
simultaneously. The original PR's secrets.provider single-selector is
deliberately dropped: multi-source is first-class and a mutually
exclusive provider switch would regress that.
- fetch() only fetches; precedence/override/conflicts/environ writes stay
in the orchestrator. ErrorKind classification + remediation hints.
- apply_command_secrets() kept as a legacy shim (parser/security helpers
unchanged: HERMES_SECRET_KEY data-only key passing, cross-key misroute
guard, base64-padding disambiguation, timeout + output cap, structured-
fields-only failure logging, stderr discarded).
- Dispatch tests rewritten for the registry path incl. an explicit
two-sources-compose test; selector tests removed with the selector.
- cli-config.yaml.example + docs page (command.md), secrets index entry.
- contributors mapping for mvalentin@valensys.net -> 0xr00tf3rr3t.
Fixes the profile-clobber bug cluster at the apply_all() chokepoint so
every secret source — bundled and plugin — gets both behaviors for free:
- secrets.preserve_existing (#58073): env var names whose existing .env /
shell value always wins, even against a source with
override_existing: true. Escape hatch for per-profile platform
secrets while everything else rotates centrally.
- Profile aliasing (#51447): under a named profile, an applied
FOO_<PROFILE> var (credential-shaped suffixes only) also hydrates the
canonical FOO, so adapters/plugins that read fixed env names see the
profile's value. Direct supply beats alias; protected/claimed/
override guards all apply; secrets.profile_alias: false disables.
Reimplements the intent of PR #58085 (tianma-if, preserve_existing on the
legacy Bitwarden apply shim) and PR #51616 (LeonSGP43, profile aliasing
inside the Bitwarden backend) on the SecretSource orchestrator that
superseded those code paths.
Fixes#58073. Fixes#51447.
Co-authored-by: tianma-if <5895871+tianma-if@users.noreply.github.com>
Co-authored-by: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com>
Keep the env-presence row, but add a real Bitwarden probe so revoked or malformed tokens no longer look healthy in hermes secrets bitwarden status.
Also document the new status behavior and lock it in with a dedicated regression test.
Refs: NousResearch/hermes-agent#40275
Tested: ./scripts/run_tests.sh tests/hermes_cli/test_bitwarden_status.py tests/test_bitwarden_secrets.py
Tested: .venv/bin/python -m ruff check hermes_cli/secrets_cli.py tests/hermes_cli/test_bitwarden_status.py
Add an opt-in battery indicator to the CLI and TUI status bars, shown as
the first element and colour-coded by charge (green/yellow/orange/red, or
green while charging). Off by default and a no-op on machines without a
battery.
- agent/battery.py: shared psutil-backed reader with a short TTL cache,
category bucketing, and a compact 🔋/⚡ label. Fails open to
"unavailable" everywhere.
- CLI: /battery [on|off|status] toggle persisted to display.battery,
rendered first in every status-bar width tier.
- TUI: /battery slash command, config sync, a system.battery RPC polled
while enabled, and a pinned first segment in StatusRule.
* feat(desktop): configure repository discovery
* fix(config): preserve additive default migration
* fix(desktop): stabilize session-actions-menu gateway mock for repo-scan subscribe
projects.ts now runs $gateway.subscribe(syncReposScanning) at module load, and
nanostores fires the subscriber synchronously. session-actions-menu.test.ts
reaches projects.ts transitively via the session store but mocked
@/store/gateway without $gateway, crashing the whole desktop vitest suite
("No \ export is defined"). Simply adding $gateway: atom(null) exposed a
second issue: the synchronous subscriber calls the mock's activeGateway()
during the transitive import, before the module-level const initializes (TDZ).
Hoist the mock fns via vi.hoisted() so activeGateway is defined before the
hoisted vi.mock factory runs, and add $gateway: atom(null) to the mock. Mirrors
the self-contained mock pattern already used in projects.test.ts. Also maps the
PR author's commit email for attribution.
Supersedes #67630; incorporates review feedback from that PR.
Co-authored-by: Rudimar Ronsoni <rudimar@outlook.com>
---------
Co-authored-by: Rudimar Ronsoni <rudimar@outlook.com>
Co-authored-by: Austin Pickett <austinpickett@users.noreply.github.com>
* feat(secrets): one-command token rotation + actionable startup errors for all secret sources
When a Bitwarden machine-account token expired, users saw a raw Rust
error dump (invalid_client + Location: + backtrace hints) and the only
fix was manually editing .env or re-running the whole setup wizard.
- New `hermes secrets bitwarden token` / `hermes secrets onepassword
token`: paste a new token (masked prompt or flag), the command probes
the backend BEFORE persisting — a rejected token changes nothing; a
good one is written to .env and the fetch caches are cleared.
- New optional SecretSource.remediation(kind, cfg) hook: startup
warnings now print a '→ Run `hermes secrets <name> token`…' fix-it
line after any fetch error, for bundled AND plugin sources (generic
per-ErrorKind defaults in the ABC).
- bws stderr is summarized to its cause line (Location:/backtrace noise
dropped) and invalid_client/invalid_grant/400 identity rejects are
now classified AUTH_FAILED (was INTERNAL) with a plain-English
explanation naming the token env var.
- op whoami probe accepts a candidate token so rotation validates the
NEW credential, not the ambient one.
Additive hook with defaults — no SECRET_SOURCE_API_VERSION bump.
* docs: fix MDX parse error in secret-source-plugin hook table
Escaped backticks around a <name> placeholder made MDX parse it as an
unclosed JSX tag, breaking the docs-site build. Use a plain code span
instead.
Three related messaging-approval fixes:
1. approvals.timeout default 60 -> 300. PR #63501 collapsed the gateway
wait onto the canonical approvals.timeout (previously
gateway_timeout=300), silently shrinking messaging approval windows
to 60s. Push-notification approvals routinely arrive later than a
minute; taps landed after the wait had already failed closed.
2. Stale-tap honesty: adapters resolved the approval AFTER rendering
'<checkmark> Approved by <user>' (Telegram/Discord/Slack), or ignored a zero
resolve count (WhatsApp Cloud/Feishu). A tap on an expired prompt
claimed approval while the command had already been denied. All
button paths now resolve first and render 'Approval expired -
command was not run' when nothing was waiting.
3. Mixed-warning prompts (dangerous pattern + tirith finding) now offer
Always: the persistence layer already permanently allowlists the
pattern key and downgrades the tirith key to session scope, but the
UI hid Always whenever ANY tirith warning was present. Pure-tirith
prompts still withhold Always (content findings are session-max by
design), and Smart-DENY overrides remain once-only.
* fix(billing): rename user-facing "terminal billing" copy to Remote Spending
The capability was renamed Remote Spending on the portal (consent CTA:
"Allow Remote Spending"; per-terminal states Granted/Stopped), but the
terminal, desktop, and docs still said "terminal billing" everywhere.
- Feature name: Remote Spending in titles/labels, lowercase mid-sentence.
- Step-up action verb is now "allow", matching the portal consent CTA.
- Kill-switch-off recovery copy points at the actual control ("a billing
admin can turn it on from the portal's Hermes Agent page") instead of
the dead-end "manage it on the portal".
- Per-terminal revoke copy uses the portal vocabulary ("stopped").
- Wire identifiers (cli_billing_enabled, cli_billing_disabled, ...) are
unchanged; copy, comments, docs, and test expectations only.
* fix(billing): correct the post-step-up denial diagnosis + finish the desktop rename
Adversarial review findings: (1) a repeated insufficient_scope after a
successful step-up is a per-terminal authorization failure, but the copy
blamed the org kill-switch and pointed at the wrong recovery control —
now: "Remote Spending still isn't active for this terminal — the
authorization didn't take. Retry, or make this change on the portal."
(2) the desktop step-up flow started in Remote Spending vocabulary but
finished in "billing management access" — renamed both end states.
(3) prettier formatting on the touched files (matches the post-merge
fmt bot).
Interrupting a busy turn with the Stop button (or Esc) settles the
session to idle, and the edge-independent auto-drain immediately submits
the head of the composer queue. The user pressed Stop to halt the agent,
but it looks like Stop skipped the current turn and kept going — and the
queued text is hard to find, since its only surface is the collapsed
'N queued' pill above the composer.
The old userInterruptedRef latch (a23728dcc) fixed this but was removed
in #40221 because it also suppressed the drain that send-now-while-busy
depends on. This reintroduces the halt with source awareness instead of
a blanket latch:
- Explicit halts (Stop button, composer Esc, chat-focus Esc, the
streaming message's hover Stop, runtime cancel) park the session's
queue before interrupting. Parked queues are skipped by both
auto-drain paths (mounted ChatBar + background drainer).
- Interrupts that exist to advance the queue (send-now-while-busy)
unpark first, so the settle drain they rely on still flows.
- The park lifts on any renewed intent: resume, a manual drain (Enter
on empty composer or the per-row send arrow), queueing a new prompt,
or emptying the queue. It migrates with entries on a runtime re-key
and is deliberately not persisted (a fresh process starts unparked).
- The queue panel expands on park, switches to 'N Queued — paused' with
a pause icon, and grows a Resume action, so the held prompts are
visible instead of reading as vanished.
Store contract, hook wiring, and background-drain coverage included;
docs updated.
Follow-up to the salvaged #52552 and #53083 commits:
- Rework the Matrix PLATFORM_HINTS entry around what the adapter actually
emits: headings, numbered lists, blockquotes, strikethrough-free markdown
all render (the adapter converts them to sanctioned HTML). Keep the
genuinely valuable guidance: no Markdown tables (Element X / Beeper /
mobile clients don't render HTML tables — cells collapse into one line),
no spoilers/checkboxes/~~strikethrough~~ (not converted by
python-markdown), prefer descriptive link text.
- Regression test: hint must steer models away from tables.
- Fix test_long_response_split_preserves_thread_context to derive its
payload size from the adapter's configurable limit instead of assuming
the old hardcoded 4000.
- Document matrix.max_message_length in the Matrix docs page.
- Contributor mapping for RKelln.
Slow language servers (tsserver on large projects especially) publish
diagnostics long after an edit. The client's wait/report path had three
holes that together surfaced the PREVIOUS edit's errors as if they were
current ("ghost diagnostics"), sending the agent chasing errors it had
already fixed:
1. open_file only cleared the diagnostic stores on first open — on the
didChange path (every subsequent edit) stale push/pull entries
survived.
2. wait_for_diagnostics' predicates were satisfiable by that leftover
state (`path in _published`, `path in _pull_diagnostics`), so the
"wait" often returned instantly with old data.
3. diagnostics_for merged the stale push store unconditionally, so even
a fresh clean pull got the old error merged back in.
Fix: anchor freshness on a per-file didChange timestamp.
- Pull results record their request send-time and are dropped when a
didChange raced past them; the pull store is invalidated on every
change, not just first open.
- wait_for_diagnostics now returns bool (fresh data vs timeout), only
counts pushes published at/after the change (and version >= ours when
the server echoes versions), and accepts an explicit timeout — the
user's lsp.wait_timeout config now actually controls the inner wait
budget instead of only the outer thread-join.
- diagnostics_for(fresh_only=True) excludes stores that predate the
latest change; all manager report paths use it.
- On timeout the manager returns [] ("no data") instead of stale
state, logs a WARNING via eventlog, and does NOT mark the server
broken — slow is not dead.
- seed-on-first-push no longer marks the file published, so the TS
seed push can't satisfy a waiter.
Tests: new "stale" and "slow_push" mock-server scripts model the slow
tsserver, plus client- and service-level regression tests
(tests/agent/lsp/test_stale_diagnostics.py).
The DingTalk docs offer gateway.platforms.dingtalk.extra.allowed_users
as the config.yaml alternative to DINGTALK_ALLOWED_USERS. The adapter
honors it (_load_allowed_users reads PlatformConfig.extra), but gateway
authorization (_is_user_authorized in gateway/authz_mixin.py) only
consults the env var, and load_gateway_config() bridged the allowlist
to the env var only from a top-level dingtalk: block. A nested-only
allowlist therefore passed the adapter and was then denied at the
gateway - listed users fell through to pairing/default-deny in DMs.
Extend the DingTalk YAML->env bridge to fall back to the merged nested
platform config (gateway.platforms / platforms), mirroring the existing
platforms.discord.extra.allow_from precedent. Precedence is unchanged:
an explicit DINGTALK_ALLOWED_USERS env var still wins, then the
top-level dingtalk: block, then the nested extra.
Also correct the docs' claim that the two allowlists are "merged" when
both are set - that behavior never existed (the doc line came from a
docs-only sweep); the effective result is the intersection of the two
gates, so the docs now recommend configuring one or the other.
Repro (before): config.yaml containing only the nested allowlist ->
adapter._is_user_allowed("user-id-1") is True but
runner._is_user_authorized(...) is False. After: both True; unlisted
users are still denied.
developers.cloudflare.com/cloudflare-one/connections/connect-networks/
returns 301 Moved Permanently to
/cloudflare-one/networks/connectors/cloudflare-tunnel/ after Cloudflare
reorganized the Cloudflare One docs. The new page is the named-tunnel
workflow this paragraph points readers at.
The copy-paste config sample had reasoning_effort: low active, which
would silently downshift effort for anyone pasting the block. Keep it
commented like other optional keys. Also add the contributor email
mapping for the salvage.
grok-4.5 is xAI's newest release (their versioning is non-monotonic:
4.5 > 4.20) and is the model xAI's own docs use for the server-side
x_search tool. Users who explicitly pinned x_search.model keep their
choice; everyone else picks up the new default via the config
deep-merge — no _config_version bump needed.
- tools/x_search_tool.py: DEFAULT_X_SEARCH_MODEL
- hermes_cli/config.py: DEFAULT_CONFIG x_search.model + comment
- agent/reasoning_timeouts.py: 300s stale-timeout floor entry for
grok-4.5 (grok-4.20-reasoning entry kept for pinned users)
- docs: x-search.md en + zh-Hans (config sample + troubleshooting)
- tests: default-model assertion + timeout-floor positive case
* feat(delegation): live-viewable subagent transcripts for delegate_task
Each child now streams an append-only, human-readable log to
<hermes_home>/cache/delegation/live/<delegation_id>/task-<n>.log while it
runs, and the dispatch return includes the paths so the caller can tail
them immediately instead of waiting blind for the consolidated summary.
- New tools/delegation_live_log.py: LiveTranscriptWriter (per-event append
+ flush, one-line rendering with truncation, never raises into the agent
loop), wrap_progress_callback (tees the child's existing
tool_progress_callback events into the log, preserves the _flush
contract), dispatch-time creation with pre-headered files so tail -f
attaches immediately, manifest.json (goals/task count/per-task status),
and 7-day retention pruning on new dispatches.
- delegate_task: wraps each child's progress callback with the writer;
sync results and background dispatch responses gain live_transcripts
(+ hint field on dispatch); per-task result entries carry
live_transcript; transcripts finalized with exit-reason markers.
- async_delegation: dispatch_async_delegation_batch accepts an optional
delegation_id so the live/ dir name matches the returned handle; the
completion event carries live_transcripts.
- process_registry: consolidated batch-completion block references each
task's live transcript path.
- Tool schema description documents the live_transcripts return surface;
docs gain a 'Live Transcripts' section with a tail -f example.
Placement under cache/delegation means the logs are mounted read-only
into remote terminal backends for free. Side-channel only: zero changes
to message content, so prompt caching is unaffected. Transcript-OUT only
— no overlap with the subagent control surfaces of PR #66046.
* fix(delegation): label the kickoff transcript line as user — it is the child's one user message
A final response generated but not confirmed-delivered was the one
artifact the gateway could lose without a trace: crash or planned
restart between finalize and platform ACK dropped it silently, and the
resume path re-ran the whole turn at full cost (#58818 P1, #41696,
#63695's gateway half).
gateway/delivery_ledger.py records each outbound final response in
state.db (same conventions as the async-delegation ledger: WAL, owner
pid + process-start-time liveness, bounded retention):
pending -> attempting -> delivered | failed
startup sweep on dead-owner rows -> redeliver | abandoned
Contract (the lessons from the closed delivery-outbox attempt #61790):
- obligation recorded BEFORE the first send attempt; cleared only on
SendResult.success (destination acceptance, #51184)
- ambiguity is labeled, never silently retried: rows that were mid-send
when the process died redeliver with a visible '♻️ Recovered reply —
may be a duplicate' prefix (honest at-least-once)
- stable ids from session_key + inbound message id + content, so
distinct threads/topics can never collide
- poison rows bounded: 3 attempts / 24h freshness -> abandoned; claim
atomically re-stamps ownership so racing sweeps can't double-claim
- redelivery clears resume_pending for the session so the resume path
never re-runs a turn whose answer the ledger already holds
- best-effort everywhere: ledger failure can never block or delay a send
- slash-command/ephemeral/empty responses are not recorded; cron and
proactive delivery stay on DeliveryRouter (separate subsystem)
Config: gateway.delivery_ledger (default on; no version bump needed).
Validation: 30 ledger+producer tests; 352 blast-radius gateway tests
green; cross-process E2E (record in process A, kill it mid-send, claim
+ marker + redeliver in a fresh process B against the same state.db).
Add an end-to-end developer guide for extending the native Hermes Desktop
app introduced in #60638: the HermesPlugin contract, PluginContext, every
contribution area (panes, routes, sidebar nav, status/title bar, palette,
keybinds, themes, composer, mount-scoped Contribute), the host API, the
React Query + nanostores data layer, the UI kit + theme variables, the
scoped ctx.rest/ctx.socket backend (plugin_api.py under /api/plugins/<id>)
and its separate enable gate, Settings/defaultEnabled/storage, bundled
plugins, the security model, pitfalls, and a full reference.
- Register the page in the sidebar under Extending -> Plugins.
- Disambiguate from the unrelated web-dashboard plugin SDK from both
directions, and add a map-table row + a desktop user-guide pointer.
- Fill the gaps in the agent-facing hermes-desktop-plugins skill
(ctx.rest/socket + backend, React Query, defaultEnabled, Contribute)
and point it at the new reference so agents know the SDK when writing
addons.
Fixes the two review defects that kept PR #29923 open, plus docs:
- gateway: exclude --once from the session-store write-through. The
once-override lived only in memory before, but the write-through
persisted it, so a gateway restart before the finally-restore
rehydrated a supposedly one-turn model permanently.
- TUI: skip _sync_agent_model_with_config while a one-turn restore is
pending. The once-model is deliberately not pinned as a session
model_override, so the config sync saw a model mismatch and clobbered
the once-override back to the config model before the turn ran.
- tests: real _handle_model_command drive asserting --once never
touches set_model_override while --session still does; restore-pop
idempotency.
- docs: /model --once in configuring-models.md with an honest
prompt-cache cost note (one-shot switch breaks the cached prefix
twice; wins for short sessions and cheap-to-expensive escalation).
Route recovered messages through the live Discord ingress policy, preserve dedup and completion invariants, bound and retain the recovery ledger, and expose the opt-in config with docs and backup coverage.
Builds on the salvaged typing_status_text plumbing (PR #62007): instead
of a static 'is thinking...', Slack's assistant status line now updates
live as the agent works — 'is running pytest tests/…', 'is reading
docs/api.md…' — and reverts to the static text between tool calls.
Mechanics:
- agent/display.py: build_status_phrase() derives a <=49-char present-
tense phrase from the existing _TOOL_VERBS table (+ 'is using <name>'
for plugin/MCP tools; None for _thinking).
- base adapter: supports_status_text capability flag + set_status_text()
per-chat store, cleared when the typing loop winds down.
- Slack adapter: send_typing() renders the live phrase when set, falling
back to typing_status_text then 'is thinking...'.
- gateway/run.py: progress_callback stashes the phrase on tool.started
and clears on tool.completed. Rendering rides the existing
_keep_typing refresh cadence — zero additional Slack API calls, no
rate-limit exposure. Works with tool_progress: off (Slack default);
the callback is now armed whenever the adapter supports status text.
- display.live_status config (full|verb|off, default full): 'verb' hides
argument previews for shared/customer-facing channels.
Also fixes a latent crash in the cherry-picked from_dict: malformed
non-dict 'extra' sections broke typing_status_text resolution (uses the
already-coerced extra dict).
Design notes: status text is a side-effect display channel only — never
enters the transcript, no prompt-cache impact. Lifecycle guarantees from
the stuck-status fix family are preserved (per-thread tracking,
clear-on-finish via existing stop_typing paths). Related: #45109
(closed; same direction via lifecycle states), #59010/#51363 (native
task cards — complementary, larger scope).
Adds PlatformConfig.typing_status_text for the two platforms that render
text for the working-state line: Slack's assistant.threads.setStatus
status (hardcoded 'is thinking...') and Google Chat's visible marker
message (hardcoded 'Hermes is thinking…'). None keeps each platform's
built-in default; to_dict omits the field when unset so existing configs
serialize unchanged. Plumbing mirrors typing_indicator exactly (typed
field, from_dict extra fallback, shared-key bridge).
Also documents that Slack's status line requires the assistant:write
scope — without it setStatus fails silently and Slack shows its own
generic placeholder, which previously made the behaviour undiagnosable
from config alone.
Replace REST-based Discord liveness probe with local WebSocket/heartbeat
state detection. REST success doesn't prove Gateway event delivery — a
half-closed WebSocket can leave Bot.start() alive while REST returns 200.
Now samples ready/open/ACK state and heartbeat latency; consecutive
unhealthy samples emit one retryable fatal code so GatewayRunner rebuilds
the adapter through the existing reconnect path.
Also fixes three lifecycle gaps in the recovery path:
1. asyncio.wait_for() can remain blocked if adapter cleanup swallows
cancellation — now uses bounded asyncio.wait() with task detachment.
2. Multiplexed secondary-profile adapters had no profile-scoped reconnect
owner — now uses one runner-owned reconnect slot per profile.
3. An in-flight turn could send its final text through the disconnected
adapter after a replacement was registered — now resolves the live
same-profile replacement for unsent final responses only (message IDs
never migrate, edits/deletes stay on the old transport).
Adds an opt-in Linux/systemd event-loop watchdog (gateway.systemd_watchdog_seconds,
default 0) for the failure mode where the whole asyncio loop stops making
progress and no in-process liveness task can run. stdlib-only sd_notify,
Type=notify/WatchdogSec generation, READY/STOPPING lifecycle.
Co-authored-by: 王鑫 <wx.xw@bytedance.com>
Follow-up to PR #66479 salvage. The three new env vars
(HERMES_LOCAL_STREAM_STALE_TIMEOUT, HERMES_GATEWAY_MAX_STARTS,
HERMES_GATEWAY_START_WINDOW_S) were introduced as bare env-var reads with no
config.yaml surface or documentation — violating the .env-is-for-secrets-only
policy (behavioral settings must live in config.yaml, bridged to env internally).
- config.yaml: add gateway.respawn_storm {max_starts, window_seconds} to
DEFAULT_CONFIG, mirroring the existing restart_loop_guard pattern.
- gateway.py: read config.yaml first, env vars override as escape-hatch.
- environment-variables.md: document all three new env vars, noting the
config.yaml alternative for the gateway ones.
- chat_completion_helpers.py: cross-reference the env-var docs from the
local stale timeout comment.
The reported confusion was an unblocked task 'unpredictably' ending up in
triage. unblock itself only ever routes to ready/todo; a subsequent same-cause
re-block hitting BLOCK_RECURRENCE_LIMIT is what escalates to triage. Document
this deterministic loop-breaker at the human-facing lifecycle level so users
stop reading it as an LLM decision.
- codex-app-server-runtime.md: add a Live display section covering the
stream/reasoning/tool-card bridge and show_commentary gating.
- release.py: AUTHOR_MAP entries for HaiderSultanArc, jjadeo-oss, juanfradb
(the latter two for forthcoming follow-up salvages of #62396 / #18050).
Community feedback (@LSanapalli on X): the inline task-creation form is
cramped inside a ~280px column with no way to resize; board-level
workspace defaults can't be changed after board creation; and users
believe they must block a task, comment, then unblock just to talk to
a worker.
- Create-task dialog: replace the inline column form with a centered
modal (reuses hermes-kanban-dialog chrome, 36rem wide) with labeled
fields for title, assignee, priority, skills, workspace kind/path,
goal mode, and parent task. Same request shape; Enter/Escape behavior
preserved; submit disabled until a title is present.
- Board settings dialog: new Settings button in the board switcher opens
a modal to edit display name, description, and the board-level default
project directory (default_workdir). PATCH /boards/:slug now accepts
default_workdir (validated absolute existing dir; empty string clears;
omitted leaves unchanged) and returns the recomputed
default_workspace_kind so task-creation defaults follow immediately.
- Comment workflow hint: the task drawer's comment box now explains that
comments land on the thread immediately and reach the worker on its
next run/kanban_show() — no block/unblock dance needed — with a fuller
tooltip for when blocking IS the right tool.
- i18n: new keys optional in the kanban namespace with English fallbacks
in the bundle (established pattern; avoids churning 17 locale files).
- Docs: dashboard section updated for the dialog + Settings button.
* feat(browser): store full snapshots on truncation; make eval denylist opt-in
Two harness fixes motivated by BU_Bench results where fixed-verb + lossy
observation cost Hermes heavily vs code-driven browser agents:
1. Snapshot truncation no longer loses content. When a snapshot exceeds
the 8000-char threshold, the complete accessibility tree is saved to
cache/web (same truncate-and-store pattern as web_extract) and the
truncated view / LLM summary includes the file path plus a ready-made
read_file call. Element refs beyond the cut are recoverable without
re-snapshotting. Stored copies are force-redacted and capped at 2MB;
content-hash filenames dedupe repeated snapshots of the same page.
2. The browser_console(expression=...) sensitive-primitive denylist is
now opt-in via browser.restrict_evaluate (default false). The
names-based denylist blocked legitimate DOM extraction — any selector
or expression containing 'fetch', 'cookie', 'input', etc. — which
crippled the agent's only programmatic page-inspection path. The
SSRF/private-URL egress guards in _browser_eval are independent of
this policy and remain always-on. browser.allow_unsafe_evaluate keeps
its meaning (bypass the denylist) for configs that already set it.
* test: update None-guard test for stored-snapshot pointer in _extract_relevant_content
test_normal_content_returned pinned the exact return value; the summary
now carries a pointer to the stored full snapshot. Assert the summary
passes through and the pointer is present instead.
* feat(browser): align snapshot threshold with web_extract's 15k char budget
SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching
web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract
truncate-and-store paths give the model the same per-page budget.
_truncate_snapshot's default max_chars now follows the constant.
Invariant test added; docs (en+zh) and CLI tip updated.
Commentary delivery is on by default; users who find the extra mid-turn
narration noisy can set display.show_commentary: false to restore the
previous behavior (commentary routed to the reasoning channel, visible
only with show_reasoning).
- hermes_cli/config.py: display.show_commentary default true
- agent/agent_init.py: wire config -> agent.show_commentary
- run_agent.py: gate structured commentary extraction on the flag
- agent/codex_runtime.py: gate live-stream commentary callback (falls
back to legacy reasoning-channel routing when off)
- docs + 2 tests (interim path off, live stream fallback)
Also adds AUTHOR_MAP entries for davidrobertson and 100yenadmin.
Add a developer-guide page for running the Ink TUI and Electron desktop
app from a git worktree without a full npm install per checkout, via the
htui/hgui shell helpers that share node_modules from a canonical deps
checkout by symlink (falling back to a local npm ci when the lockfile
diverges). Registers it in the sidebar, cross-links from the TUI and git
-worktrees pages, and documents the previously-undocumented
HERMES_DESKTOP_PYTHON / HERMES_DESKTOP_DEV_SERVER env vars the desktop
backend reads.
Replaces moonshotai/kimi-k2.6 (recommended) and moonshotai/kimi-k2.7-code
with moonshotai/kimi-k3 in both curated lists, regenerates the published
model-catalog.json manifest, and updates the docs example manifest (en+zh).
kimi-k3 verified live on both endpoints (Nous Portal /v1/models and
OpenRouter /api/v1/models; 1M context, $3/$15 per Mtok). Family-prefix
matching already covers k3 in moonshot_schema, cache policy, and context
heuristics — no code changes needed there.
hermes update ran TWO separate pre-update backup mechanisms: the
config-gated full zip (updates.pre_update_backup, default off) and an
unconditional quick state snapshot added for #15733 that ignored the
user's setting entirely. On a large state.db (observed: 24 GB) the
'cheap' snapshot silently added ~60s to every update and ate 24 GB of
disk in state-snapshots/.
Now there is ONE mechanism, gated by updates.pre_update_backup with
three modes:
- quick (new default): state snapshot of critical small files (pairing
JSONs, cron jobs, config, auth, per-profile DBs). Files over 1 GiB
are skipped with a warning so a bloated state.db can never stall the
update again.
- full: the quick snapshot plus the HERMES_HOME zip (old 'true'
behavior; --backup forces it for one run).
- off: nothing runs — an explicit opt-out now disables the quick
snapshot too (--no-backup does the same per-run).
Legacy booleans are honored: true -> full, false -> off.
_run_pre_update_backup() now returns the quick-snapshot id so the
post-update cron-jobs restore safety net (#34600) keeps working; the
snapshot moved from the post-fetch site to the pre-mutation site,
which also covers the zip-fallback update path it previously missed.
_clean_reasoning_effort kept its own whitelist that stopped at 'max',
silently dropping 'ultra' from MoA slot configs. Route it through
hermes_constants.parse_reasoning_effort — the same one-source-of-truth
fix the salvaged commit applies to the gateway — so future effort
levels can't drift here either. Docs updated to list ultra.
Follow-up to salvaged PR #64012.
Adds the proxied-callback option to the remote/headless OAuth section,
links the mcp-oauth-remote-gateway skill for fully headless gateways,
and documents the WAF pitfall behind redirect_host.
PR #36051's values went stale since May 31: session-store SCHEMA_VERSION
is now 21 (PR said 14), and the dashboard ships 8 built-in themes
(PR said 7). Also document the v16/v18/v20 data migrations added since.
Cross-checked website/docs against the source at main HEAD and corrected
documented commands, env vars, config keys, headers, and default values
that don't match the code. Docs-only; no behavioral changes.
Refs #36048
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Disable address reuse so an existing family-specific listener cannot silently split traffic with the webhook server. Normalize wildcard bind hosts for local CLI URLs and align setup documentation with the dual-stack default.
Follow-up on kshitijk4poor's cherry-picked references:
- pitfalls.md rewritten from the raw-socket blender_exec() frame to the
MCP-tool frame (dropped 'MCP server is optional, talk to the socket
directly' — now the anti-pattern; dropped TCP-helper internals items;
kept all bpy/addon knowledge: empty code results in 5.x, temp-file
readback, ops-vs-data context, engine names by version, GPU setup)
- recipes.md: blender_exec -> execute_blender_code in the agent-side
verification snippet
- SKILL.md: reference-file table added to Quick Reference, version
2.1.0, kshitijk4poor added to authors
- docs page regenerated (scoped to this skill)
Fixes#62452. Two amplifiers turned one slow auxiliary route into a
per-turn multi-minute stall:
1. Fallback candidates inherited the exact effective_timeout the primary
was called with. When the primary's deadline was short (tuned or
already burned), an independently healthy fallback died on the same
clock — the reporter's 163k-token compression needed ~90s on the
fallback and got the primary's 30s, every turn. fallback_chain
entries may now declare their own 'timeout' (seconds); both fallback
candidate call sites (sync + async) resolve it via
_fallback_entry_timeout, label-scoped so only configured-chain
candidates are affected. No entry timeout → task-level timeout,
preserving existing behavior.
2. A session whose transcript structurally cannot be summarized within
the deadline re-attempted every 60s, re-burning the full timeout on
every subsequent turn. Consecutive timeout-class failures now
escalate the cooldown 60s → 300s → 900s (capped); any successful
summary or session reset clears the streak. Timeout classification
takes precedence over the streaming-closed 30s rung ('timed out'
also matches _is_connection_error) and now recognizes the SDK's
'Request timed out.' phrasing.
Fail-safe behavior is unchanged: all messages are preserved when every
candidate fails; the cooldown only spaces out retries.
Follow-ups on the salvaged #20096 profile-routing feature:
- _profile_name_for_source now returns None unless gateway.multiplex_profiles
is on. Routing stamps source.profile, which namespaces session/batch keys,
but the profile-scoped agent run only activates under multiplexing — without
the gate, configured routes with multiplexing off split batch/session keys
into agent:<profile> while the agent still ran from agent:main.
- Widen the profile-aware _text_batch_key fix from Discord to every adapter
that builds batch keys via build_session_key (telegram, whatsapp, matrix,
feishu, wecom, weixin) — routing is platform-generic, so the batch-key
namespace fix must be too.
- Downgrade the no-route-matched log from INFO to DEBUG (fired on every
unrouted inbound message).
- GatewayConfig.to_dict(): serialize profile_routes as plain dicts
(ProfileRoute dataclasses are not JSON-safe).
- Docs: correct the 'independent of multiplexing' claim in
docs/profile-routing.md (routing requires multiplexing), fix the
platform-only specificity row (0, not 1), and document profile_routes in
website/docs/user-guide/multi-profile-gateways.md.
- Tests: pin the multiplex gate (routes ignored when off, active when on,
build_source end-to-end stays in agent:main when off).
The optional blender-mcp skill predates the blender MCP catalog entry
(#64463) and taught the agent to hand-roll raw TCP JSON to the addon's
socket on port 9876 from execute_code — bypassing the catalog's version
pinning and install-time tool curation.
Reworked to v2.0.0 as the companion skill for the catalog entry:
- prerequisites now go through 'hermes mcp install blender'
- interaction surface is the four curated MCP tools, not a raw socket
- keeps the valuable content: addon setup, bpy recipes (materials,
keyframes, render-to-file), pitfalls (timeouts, absolute paths,
object mode), plus new pitfalls (xvfb headless, no-sandbox warning,
remote-host path resolution)
- explicit anti-pattern note: do not hand-roll TCP to 9876
- description shortened to <=60 chars per skill authoring standards
alireza78a's original bpy patterns and pitfalls are preserved and
credited. Docs page regenerated via generate-skill-docs.py (scoped to
this skill only; unrelated generator drift left untouched).
The remote model catalog (website/static/api/model-catalog.json) now labels
exactly one entry per provider block with "default": true — z-ai/glm-5.2 for
both OpenRouter and Nous Portal. That labeled entry is the model Hermes
silently lands on when the user never picked one, and it can be rotated by
editing the manifest alone: no release needed.
- model_catalog.py: get_default_model_from_cache() reads the label from the
in-process/disk cache only — never triggers a network fetch, so hot
resolution paths (agent build, gateway session setup) stay network-free.
- models.py: get_preferred_silent_default_model() resolves catalog label
first, PREFERRED_SILENT_DEFAULT_MODEL constant second (offline/fresh
install). _PROVIDER_SILENT_DEFAULT_OVERRIDES dict replaced by
_SILENT_DEFAULT_PROVIDERS routing through the shared resolver.
fetch_openrouter_models() preserves the "default" badge through live
/v1/models refreshes so the picker shows it.
- scripts/build_model_catalog.py: generator emits the default label so
regeneration can't drop it.
- website/docs/reference/model-catalog.md: schema documents the new field.
- Salvaged from PR #61141 (@HumphreySun98): bare-provider /model switches
(/model nous) route through the cost-safe default instead of curated
entry [0].
- tests: catalog-label precedence, constant fallback, stale-label fallback,
cache-only (no network) guarantee, and a shipped-manifest contract test
pinning the labeled entry to PREFERRED_SILENT_DEFAULT_MODEL.
E2E (temp HERMES_HOME): fresh-install constant fallback, shipped-manifest
label read, release-free rotation (relabeled cache -> new default across
models.py, tui_gateway, and gateway empty-model paths) all verified.
The just-merged auxiliary.<task>.reasoning_effort shorthand applied
ensemble-wide to MoA (one value for every advisor) — wrong granularity.
Per-slot preset config supersedes it:
moa:
presets:
deep_review:
reference_models:
- {provider: ..., model: ..., reasoning_effort: low}
- {provider: ..., model: ..., reasoning_effort: xhigh}
aggregator:
{provider: ..., model: ..., reasoning_effort: high}
- Remove reasoning_effort from the moa_reference/moa_aggregator
DEFAULT_CONFIG blocks; _get_task_extra_body now warns-and-ignores the
key on MoA tasks, pointing at the preset config
- Guard tests: MoA aux blocks must not regrow the key; task-level value
is rejected with the pointer warning
- Docs: configuration.md notes the MoA exception and links the MoA page
Every auxiliary task block (vision, web_extract, compression,
title_generation, curator, background_review, moa_reference, ...) now
accepts a reasoning_effort shorthand:
auxiliary:
compression:
reasoning_effort: low
vision:
reasoning_effort: none
_get_task_extra_body() folds it into extra_body.reasoning, which every
auxiliary wire already translates: chat.completions passes it through,
the Codex Responses adapter maps it to top-level reasoning/include, and
the Anthropic auxiliary adapter now forwards it into
build_anthropic_kwargs(reasoning_config=...) (previously hardcoded None).
An explicit extra_body.reasoning on the same task wins over the
shorthand. Invalid levels are ignored with a warning. Empty string
(the shipped default) is a no-op — zero behavior change.
Config: reasoning_effort added to all 16 auxiliary task blocks in
DEFAULT_CONFIG (no version bump — deep-merge handles new keys).
Add agent.reasoning_overrides dict to config.yaml. Users can now set
a reasoning_effort per model, overriding the global agent.reasoning_effort.
Example:
agent:
reasoning_effort: "medium" # global default
reasoning_overrides:
"openrouter/anthropic/claude-opus-4.5": "xhigh"
"openai/gpt-5": "low"
"claude-sonnet-4.6": "high" # bare model name also works
The helper is spelling-tolerant: override keys match regardless of
provider prefix or dots-vs-dashes normalization, so users can write
keys in any sensible form and they'll match.
Resolution priority:
1. Session-scoped /reasoning --session override (gateway only; unchanged)
2. Per-model override from agent.reasoning_overrides (spelling-tolerant)
3. Global agent.reasoning_effort (existing)
4. Provider default (unchanged)
Wired into:
- CLI startup (cli.py)
- Messaging gateway agent construction (gateway/run.py)
- Desktop/TUI _load_reasoning_config (tui_gateway/server.py)
- Cron job scheduler (cron/scheduler.py)
- /model mid-session switch (agent/agent_runtime_helpers.py)
+ _primary_runtime now tracks reasoning_config for correct fallback recovery
- Fallback activation (agent/chat_completion_helpers.py::try_activate_fallback)
+ Re-resolves reasoning_config for the fallback model (best-effort)
Closes#21256 (per-model reasoning_effort defaults).
Note: no hermes config set agent.reasoning_overrides.<model> support;
users edit the YAML directly. _set_nested splits on "." and would
corrupt model keys containing version dots.
Return actionable errors when HERMES_WRITE_SAFE_ROOT blocks a path instead of
labeling every denial as a protected credential file. Wire the helper through
write_file, patch, delete/move, and the Copilot ACP shim; sync docs examples.
Document that safe-root violations are hard-blocked (not approval-gated),
add a security guide section for write_file/patch guards, and link cron
and verifier docs so users trust the footer over agent summaries.
- Nudge text now warns that repeated protocol violations will block the
task and require manual intervention, so the model understands the
consequence of ignoring the nudge.
- Kanban docs restructured to clearly separate the two defense layers:
agent-side prevention (nudge, from #64350) and dispatcher-side
recovery (bounded retry, from this PR).
- Two new integration tests verifying the nudge mentions blocking and
that the agent-side and dispatcher-side budgets are independent.
Fold in kevinb361's suggested lifecycle wording (#61817 conceded in favor of
this PR) and update the second stale site his sweep didn't cover: the
task-events table still said the dispatcher 'auto-blocks immediately instead
of retrying'. Both now describe the violation-only streak: protocol_violation
fires on every violation (its payload marker feeds the budget), below-budget
runs return the task to ready, and gave_up + auto-block happen only when the
consecutive streak reaches _PROTOCOL_VIOLATION_FAILURE_LIMIT (default 3,
per-task max_retries overriding).
OpenAI lets ChatGPT-plan Codex users bank rate-limit reset credits, but
until now they could only be redeemed from the Codex CLI/app or the
website. This wires the same backend API into Hermes:
- /usage on the openai-codex provider now shows "You have N resets
banked - use /usage reset to activate" (parsed from the
rate_limit_reset_credits field the /usage endpoint already returns).
- New /usage reset subcommand (CLI + gateway) redeems one banked
credit via POST .../rate-limit-reset-credits/consume with a UUID
idempotency key, mirroring codex-rs backend-client semantics
(PathStyle /wham vs /api/codex, ChatGPT-Account-Id header,
reset/nothing_to_reset/no_credit/already_redeemed outcomes).
- Guard: redemption is refused while no rate-limit window is fully
exhausted, since a banked reset restores the FULL 5h + weekly
allowance and spending it early wastes it. /usage reset --force
overrides. Zero banked credits and non-codex providers are refused
with clear messages; nothing_to_reset reports the credit was NOT
spent.
- i18n: new gateway.usage.unknown_subcommand / reset_wrong_provider
keys across all 16 locales; docs updated (cli.md, messaging index).
Tested with unit tests plus a real-socket E2E against a local fake
Codex backend exercising redeem/guard/force and the /usage hint.
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.
Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
survives restarts for repeat testing
In the Nix devShell the script is available as 'sandbox'.
Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
Follow-up to #60944 (webhook payload filters and route scripts):
- reference/cli-commands.md (en+zh): document the new --script option on
'hermes webhook subscribe'
- zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script
Filters/Transforms sections plus the filters/script route properties
(the salvage shipped English-only docs)
- hermes-agent skill webhooks reference: teach the agent the filters/
script surface so agent-driven subscriptions can use them
Docs portion of PR #57067: 'bot connects but never replies' section
pointing at the gateway.log warning and the allowlist/policy knobs.
Co-authored-by: ooovenenoso <120500656+ooovenenoso@users.noreply.github.com>
Restructures the five parallel export sections into a single 'Export
Sessions' section: a format table (jsonl/md/qmd/html/trace + --only
user-prompts), one shared-filters paragraph covering all formats, and
per-format subsections nested beneath. EN + zh-Hans.
* feat(trace): upload sessions to HF Agent Trace Viewer
Salvage trace upload as a smaller CLI-first feature: deterministic Claude Code JSONL export, fail-closed redaction, lazy Hugging Face dependency, and no gateway slash-command wiring.
* chore(trace): drop external porting references from docstrings
Describe the trace-upload design in Hermes' own terms.
* feat(sessions): fold trace upload into 'sessions export --format trace'
Integrates the HF Agent Trace Viewer exporter (PR #36145) onto the
unified export surface instead of a separate 'hermes trace' subcommand:
- --format trace: Claude Code JSONL to stdout/file, or one
<id>.trace.jsonl per session for filtered bulk export; defaults to
the most recent session when no --session-id/filters given.
- --upload pushes to the user's private HF traces dataset (--public to
opt out of private); reads HF_TOKEN with guided setup when missing.
- traces are secret-redacted by default (force mode); --no-redact opts
out after review; redaction failure blocks export (fail closed).
- hermes_cli/trace.py + subcommands/trace.py removed; agent/trace_upload.py
is the single engine. Docs EN + zh-Hans; 4 new CLI tests.
The salvaged SelfHostedBackend made self-hosted servers reachable via
mem0.json / MEM0_HOST, but the setup wizard still offered only Platform
and OSS — exactly the gap users hit (Discord report: 'At memory setup
there's only 2 options'). Adds a third wizard mode:
- interactive picker: Platform / Self-hosted server / Open Source
- non-interactive: hermes memory setup mem0 --mode selfhosted
--host http://... [--api-key ...] [--dry-run]
- host -> mem0.json (behavioral), API key -> .env as MEM0_API_KEY
(secret), optional key for AUTH_DISABLED servers
- best-effort reachability check against the server, non-fatal
- README + memory-providers docs updated with the wizard path
Salvage follow-up integrating PR #30481 (@simplast) and PR #57683
(@catbearlove1-lang) into the unified export surface:
- --format html: standalone self-contained HTML transcript (single
session or multi-session with sidebar), works with all shared filters
and --redact; requires a file output path.
- --only user-prompts: prompt-only export (jsonl records or md sections)
via the shared session_export renderer; the separate export-prompts
subcommand from the original PR is subsumed by this flag.
- AUTHOR_MAP entries for both contributors; docs EN + zh-Hans.
Review follow-ups on the salvage:
- get_all() pruned from the ABC and all three backends: mem0_list (its
only caller) was removed by the recall-tuning commit, leaving new,
tested, unreachable code — including SelfHostedBackend's _MAX_TOP_K
over-fetch workaround. Tests for it dropped; fake-class stubs remain
harmlessly. (The #52921 true-total fix lives on in the PR history if
a lister ever returns.)
- The persisted rerank config key was write-only (setup prompted for it,
nothing read it). initialize() now parses it into _rerank_default and
mem0_search uses it when the model doesn't pass rerank explicitly;
per-call args still win. Guard test added.
- Platform-mode setup now warns when MEM0_HOST is set in the environment:
the json host-clear can't help there (_load_config seeds host from the
env var, docs tell users to put it in .env) — the user would silently
keep routing to the self-hosted server.
- SelfHostedBackend: connect-level retries (httpx.HTTPTransport(retries=2))
so a single transient blip doesn't count toward the provider breaker;
transport now injectable and the test helper uses the real __init__
instead of mirroring it via __new__.
- plugin.yaml description no longer leads with reranking (off by default,
platform-only); docs em-dash typo fixed.
Salvage of #55614 by @kartik-mem0 (mem0 maintainer). Adds a SelfHostedBackend
that talks to a self-hosted Mem0 Docker server over httpx (X-API-Key auth,
/search + /memories routes), gated behind `host`. Also folds in the mem0
research-team recall tuning that rides with it: rerank defaults to false across
all modes, the mem0_list tool is removed (5->4 tools), search guidance is
de-shouted, and self-hosted get_all reports the true stored total (#52921).
Supersedes the self-hosted portion of #52487 (@liuhao1024, first-submitted).
Closes#52478Fixes#52921
Review findings on the salvaged #60332 breaker, fixed as follow-ups:
- restore_primary_runtime() now resets the streak (third provider-swap
path; without it a recovered primary was short-circuited before a
single attempt and could never be re-proven healthy except via /model).
- interruptible_api_call (non-streaming) now carries the same breaker
(guard at entry, bump on stale_call_kill, reset on success). Quiet-mode
/ subagent / headless sessions — the profile most like #58962's
unattended 494-failure session — take this path and had the identical
infinite stale-retry class.
- Partial-stream stub return now resets the streak (chunks were received,
provider demonstrably responsive).
- Consolidated the triple-duplicated counter arithmetic into shared
helpers (_stale_streak/_bump_stale_streak/_reset_stale_streak/
_check_stale_giveup) with one canonical comment block; error message
now says 'consecutive stale attempts' (the counter counts kills, not
turns — a single turn can produce several).
4 new tests (restore resets / no-op restore keeps latch / non-streaming
short-circuit / non-streaming success reset).
- export now shares _add_session_filter_args / build_prune_filters with
prune/archive: AGE grammar (5h/2d/1w/ISO) on --older-than plus the full
filter set (--model, --provider, --min-messages, --min-cost, --branch,
--chat-id, ...) for both JSONL and md/qmd bulk exports; --dry-run works
on JSONL too; removes the one-off list_export_candidates helper.
- new --redact flag runs exported message content and tool output through
force-mode secret redaction (agent.redact) for jsonl, md, and qmd.
- docs EN + zh-Hans updated; new tests for AGE grammar, extended filters,
filtered JSONL, and redaction.
Replaces the parallel export-md subcommand from the salvaged commit with
a --format jsonl|md|qmd flag on the existing export subcommand, so all
session export formats share one surface. Adds AUTHOR_MAP entry.
Salvage follow-up for PR #59542 by @web3blind.
Sessions no longer auto-reset by default. SessionResetPolicy.mode now
defaults to "none" (was "both": 24h idle + daily 4am), matching the
setup wizard's existing no-reset default and community feedback that
surprise context loss hurts more than it helps.
- gateway/config.py: dataclass default + from_dict fallback -> "none";
installs whose config.yaml lacks a session_reset section stop
auto-resetting
- hermes_cli/setup.py: "Never auto-reset" is now the recommended/default
choice in hermes setup agent; stale comment updated
- docs (en + zh-Hans): default is no auto-reset, opt in via
session_reset in config.yaml
Users who explicitly configured idle/daily/both resets keep them.
--safe-mode promised to disable ALL customizations, but shell hooks
declared in config.yaml's hooks: block registered anyway —
register_from_config() runs independently of plugin discovery and
load_config() does not honor HERMES_IGNORE_USER_CONFIG. Gate it on
HERMES_SAFE_MODE at the single chokepoint so troubleshooting runs fire
zero user-configured code (plugins, MCP, and hooks).
Docs (en + zh) updated; positive + negative tests added.
The Codex gpt-5.5 compaction-threshold autoraise notice re-fired on every
agent init. Because the gateway rebuilds the agent per inbound message, the
notice spammed long-running Discord/Telegram/etc. sessions, and the only
documented remedy (`compression.codex_gpt55_autoraise false`) disables the
useful autoraise behavior itself.
Gate both emission surfaces — the CLI startup print and the gateway
`_compression_warning` replay — on a persisted per-profile marker under
`$HERMES_HOME` (`.codex_gpt55_autoraise_notice`), keyed on the from→to
percentages the notice displays. The notice now shows at most once per
profile; the autoraise still fires and `codex_gpt55_autoraise: false` still
disables it; and a later change to the raised threshold re-notifies once.
Docs updated to match.
- New developer-guide/browser-provider-plugin.md: BrowserProvider ABC
(session lifecycle, CDP contract, bb_session_id back-compat key,
raise/never-raise split between create and close/cleanup),
get_setup_schema() hermes-tools integration, discovery, checklist.
Closes the one gap in the provider-plugin family — the ABC and
ctx.register_browser_provider() existed with zero docs.
- Register the page in the Plugins sidebar subcategory.
- Extend the routing map on the Plugins landing page (both locales)
with the previously missing rows: web-search, browser, secret-source,
and dashboard-auth surfaces.
* docs(secrets): secret-source plugin developer guide + sidebar registration for 1Password page
- New developer-guide/secret-source-plugin.md: SecretSource contract
(never raises/prompts, fetch-only, timeout budget), framework-vs-plugin
ownership table, mapped-vs-bulk shape guidance, run_secret_cli()
subprocess-safety, registration + timing note, conformance kit usage,
ErrorKind reference.
- Register user-guide/secrets/onepassword in the sidebar (page shipped
in #59498 but was not listed, so it was unreachable from nav).
- Cross-link the user-guide plugin section to the new dev guide.
* docs: group all plugin guides under a Plugins subcategory in Extending
- Move guides/build-a-hermes-plugin.md -> developer-guide/plugins/index.md
(both locales) and make it the category landing page (slug pinned to
/developer-guide/plugins).
- New sidebar subcategory Developer Guide > Extending > Plugins holding
the general guide + all 8 provider-plugin docs (llm-access, memory,
context-engine, secret-source, model, image-gen, video-gen, web-search);
provider-doc URLs unchanged.
- Client redirect /guides/build-a-hermes-plugin -> /developer-guide/plugins.
- Update 30 cross-links across both locales.
The 1Password secret source resolves op:// references using
OP_SERVICE_ACCOUNT_TOKEN read from os.environ. Under systemd the gateway
gets that token via EnvironmentFile, but cron jobs, subprocesses, CLI
runs, macOS launchd, and Docker containers spawn fresh interpreters with
no inherited shell state — so they silently failed to resolve any
reference and fell back to empty strings.
Two patches close the gap, matching Bitwarden's reliability guarantees:
1. env_loader: auto-load ~/.hermes/.op.env after .env so the gitignored
bootstrap token is available everywhere. override=False plus an
explicit guard ensure it never clobbers a token already in env (e.g.
from a systemd EnvironmentFile, which keeps precedence).
2. credential_pool: _get_env_prefer_dotenv() now prefers the resolved
value in os.environ when .env still holds a raw op:// reference,
instead of handing a URL to provider auth. Non-op:// values keep the
existing .env-takes-precedence behaviour.
Also gitignore .op.env, document the three bootstrap-token options, and
add tests covering auto-load, no-override, and the resolved-vs-raw
precedence (plus regression guards).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve provider credentials from 1Password op://vault/item/field references
at startup via the official `op` CLI, alongside the existing Bitwarden source.
Users map env-var names to references in secrets.onepassword.env; after .env
loads, each is resolved with `op read` and injected into os.environ. Auth is
whatever `op` already uses (service-account token or desktop/interactive
session) — Hermes never authenticates or installs `op` itself.
Startup-safe and fail-open: a missing binary, expired auth, a bad reference,
or an empty value each warn and fall back to existing credentials, never
blocking startup. Successful, complete pulls are cached in-process and on disk
(<hermes_home>/cache/op_cache.json, 0600) via the shared DiskCache; only
secret values are stored, never the token (auth is fingerprinted into the
key). Adds `hermes secrets onepassword {setup,status,set,remove,sync,disable}`
(aliases op/1password), config defaults, the cli-config example, docs, and
hermetic tests.
Hardening applied across both backends in env_loader: each source runs in its
own guard, config sections are coerced to dict, and cache_ttl_seconds is
coerced defensively — so a malformed secrets: section can't abort startup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #59332 targeting the remaining PERCEIVED first-token latency
(the wire streaming was already per-token; these fix what the user sees):
1. display.show_reasoning default ON. On thinking models the reasoning
phase streams for tens of seconds; with the display off users stare
at a spinner the whole time and read it as a stall. Flipped in
DEFAULT_CONFIG, load_cli_config defaults, tui_gateway raw-YAML
fallbacks, and the hermes setup status line (all four read sites kept
in sync). Gateway per-platform defaults intentionally stay off —
messaging chats shouldn't fill with thinking text. /reasoning hide
still turns it off and persists.
2. Response box force-flushes long partial lines. _emit_stream_text only
painted on newline, so a response opening with a long paragraph
stayed invisible until the first \n — seconds of blank box. Now
partial lines wrap at terminal width and paint as tokens arrive
(mirrors the reasoning box's 80-char force-flush that existed since
day one). Table blocks remain batch-aligned; no content loss at wrap
boundaries (regression tests added).
3. hermes_time timezone resolution uses read_raw_config (mtime-cached +
libyaml C loader) instead of a raw yaml.safe_load of config.yaml
(~110-140ms measured) inside the FIRST system prompt build. First
build drops 320ms -> ~155ms on a 200-skill install.
4. Stale docs: configuration.md (en+zh) still documented the 70%/90%
[BUDGET WARNING] tool-result injections. Those were removed in April
2026 (c8aff7463) precisely because they hurt task completion; current
behavior is exhaustion-message + one grace call, no mid-loop
injection, no cache impact. Docs now describe reality.
Verified: token-count compression decisions already use API-reported
last_prompt_tokens (rough estimators are preflight-only and cost ~1.7ms
even on 1.7MB histories — not worth touching).
Bare 'hermes sessions prune' keeps the historical 90-day default, but any
filter — now including --source — suppresses the implicit cutoff, so
'prune --source cron' targets ALL cron sessions instead of silently only
those older than 90 days (the surprise a user hit live: 'No sessions
match ... source cron' despite plenty of recent cron runs).
- CLI preview + confirmation now show the match count plus the oldest
and newest matching session start times before deleting.
- Dashboard /api/sessions/prune mirrors the semantics: attribute filters
without an explicit older_than_days match all ages (model_fields_set
distinguishes an explicit 90 from the Pydantic default); dry_run
responses gain oldest_started_at/newest_started_at.
- Docs + argparse help updated; tests for both surfaces.
* feat(sessions): full filter surface for prune + new bulk archive subcommand
hermes sessions prune previously only supported --older-than N (integer
days) and --source — no way to target a window like 'the last 5 hours'
(e.g. a batch of CI smoke-test sessions), and no non-destructive option.
- SessionDB.prune_sessions gains keyword filters that AND together:
started_before/started_after epoch bounds, title_like, end_reason,
cwd_prefix, min/max_messages, archived tri-state. Default call is
byte-for-byte compatible (90-day cutoff, ended-only, source).
- New SessionDB.list_prune_candidates (backs --dry-run + confirmation
previews) and SessionDB.archive_sessions (bulk soft-hide via the
existing set_session_archived lineage-aware path; nothing deleted).
- CLI: prune gains --newer-than/--before/--after (durations like 5h/2d/1w,
bare days, or ISO timestamps), --title, --end-reason, --cwd,
--min/--max-messages, --include-archived, --dry-run. New
'hermes sessions archive' takes the same filters, requires at least one,
and is idempotent. Both show a preview before confirming.
- Dashboard /api/sessions/prune accepts the same filters + dry_run.
- Docs: sessions.md + cli-commands.md updated.
Filter parsing lives in hermes_cli/session_filters.py with unit tests;
DB filters covered in tests/test_hermes_state.py.
* feat(sessions): prune/archive filters for model, provider, user, chat, branch, tokens, cost, tool calls
Extends the prune/archive filter surface to everything identifiable in
the sessions table:
- --model (substring on model slug), --provider (exact on
billing_provider, case-insensitive), --user, --chat-id, --chat-type
(exact), --branch (substring on git_branch), --min/--max-tokens
(input+output), --min/--max-cost (USD, actual_cost_usd falling back to
estimated_cost_usd), --min/--max-tool-calls.
- SessionDB prune/archive/list_prune_candidates now share the filter
kwargs via **filters into _prune_filter_where (unknown names raise
TypeError); candidates listing + CLI preview now include the model.
- Any attribute filter (except legacy --source) suppresses the implicit
90-day default so 'prune --model X' matches all ages.
- Dashboard /api/sessions/prune passes the new fields through.
- Docs + tests updated (7 new DB tests, 3 new parser tests).
* fix(docker): heal pairing-dir ownership after `docker exec` writes (#10270)
The official Docker image runs the gateway as the unprivileged `hermes`
user (uid 10000) via `gosu`, but `docker exec` defaults to root. Approval
files written by `docker exec <container> hermes pairing approve <code>`
end up as `-rw------- root:root`, and the post-gosu gateway process
cannot read them. The approval is silently ignored — the user keeps
hitting 'Unauthorized user' on every message.
The entrypoint's existing top-level chown is gated on the top-level
$HERMES_HOME being mis-owned, so on warm boots (where /opt/data is
already hermes:hermes) the recursive chown is skipped — meaning a
container restart does NOT self-heal the bug either.
Three-part fix:
1. docker/entrypoint.sh: chown the platforms/pairing/ (and legacy
pairing/) subtree on every container start, regardless of the
top-level decision. The directory is tiny (a few JSON files), so
the unconditional chown is effectively free. Container restart
now self-heals.
2. gateway/pairing.py: PairingStore._load_json was swallowing
PermissionError under its bare 'except OSError' branch, which is
what made this a silent failure. Split it out: log a WARNING that
names the file, the gateway's uid, the file's owner/mode, and the
exact docker exec -u hermes workaround. Still falls back to {} so
the gateway stays up.
3. website/docs/user-guide/security.md: add a Docker tip to the
pairing-CLI section pointing users at `docker exec -u hermes …`
up front.
Reproduced end-to-end in a containerized harness — before the fix
the gateway sees 0 approved users after `docker exec` + restart;
after the fix it sees the expected 1, and the file on disk goes
from `root:root 600` back to `hermes:hermes 600` on next start.
Fixes#10270
* fix(pairing): gate os.geteuid for Windows in PermissionError warning
Adds approvals.deny to config.yaml — a list of fnmatch globs matched
against terminal commands. A match blocks unconditionally, BEFORE the
--yolo / /yolo / approvals.mode=off bypass, making it the user-editable
counterpart to the code-shipped hardline blocklist.
- Checked in both command gates (check_dangerous_command and
check_all_command_guards), after the hardline floor and sudo-stdin
guard, before the yolo bypass and permanent allowlist.
- Matching runs over the same normalized/deobfuscated command variants
as the dangerous-pattern detector, case-insensitive.
- Opt-in: empty/absent list is a no-op; behavior unchanged.
Supersedes the trust-engine approach from #21500 with a minimal
config-native design: the only capability the existing stack lacked
was deny-that-beats-yolo. Allow already exists (command_allowlist),
ask already exists (session approvals).
Follow-up to the salvaged toggle commit:
- file_tools.py / code_execution_tool.py: carry docker_network in their
container_config dicts so those environment-creation paths honor the
lockdown instead of silently defaulting back to bridge (the probe/exec
asymmetry class reported on #46358).
- docker.py: cross-process reuse now inspects HostConfig.NetworkMode when
docker_network=false and removes a mismatched (networked) container
before starting a fresh air-gapped one. Fails closed when inspect fails.
Default-network config never churns containers, so operators using
docker_extra_args --network=none are unaffected.
- tests: AST invariant that every container_config site carrying
docker_run_as_host_user also carries docker_network, plus three reuse
guard tests (reject bridge under lockdown / keep matching none /
no inspect when network enabled).
- docs: configuration.md gains terminal.docker_network + env var row.
Port from openai/codex#21069 ("Spill large hook outputs from context").
Both shell hooks and Python plugins can return {"context": "..."} from
pre_llm_call, which gets appended to the current turn's user message on
every subsequent API call. A plugin that emits a large blob inflates
every turn and blows out the prompt cache prefix.
- tools/hook_output_spill.py: shared helper that writes oversized
context to $HERMES_HOME/hook_outputs/<session_id>/<uuid>.txt and
returns a head/tail preview plus the saved path. Never raises.
- agent/turn_context.py: apply the cap at the pre_llm_call aggregation
site (moved here from run_agent.py since the original PR), covering
both Python plugins and shell hooks.
- agent/shell_hooks.py: reserve output_spill as a sub-key under hooks:
so the config block doesn't emit unknown-hook-event warnings.
- Docs: document the cap + config in build-a-hermes-plugin.md.
Config (behaviour-preserving when absent):
hooks.output_spill: enabled/max_chars/preview_head/preview_tail/directory
Tests: 14 unit tests; shell_hooks (56) and plugins (100) suites green.
E2E validated with isolated HERMES_HOME (spill, passthrough, traversal
sanitisation, reserved-key skip).
Add an opt-in toggle (require_admin_for_exec_approval, default false) that
restricts who can click Approve/Deny on a dangerous-command prompt to admins
listed in allow_admin_from. Off by default, so the v0.16-restored user-scope
behavior is unchanged. When on, the clicker must pass the normal admission
check AND be an admin; fails closed (logged) when no admins are configured.
Only ExecApprovalView is gated — model picker / clarify / update-prompt stay
user-scope.
/model switches, primary-model fallback, and credential-pool key
rotation all change the prompt-cache key (model and/or account), so
the next turn re-reads the entire conversation at full input price.
Add cost warnings everywhere docs recommend or describe these paths:
- reference/slash-commands.md: cost note on both /model rows
- user-guide/features/fallback-providers.md: warning admonition
- user-guide/features/credential-pools.md: warning admonition
- user-guide/configuring-models.md: mid-session switch warning
- guides/tips.md: expand cache tip + /model tip
- reference/faq.md: warning on the switch-back-and-forth example
- user-guide/desktop.md: composer picker bullet
- developer-guide/context-compression-and-caching.md: new
cache-aware design pattern (model identity is part of the key)
Inspired by Claude Code v2.1.199 (July 2, 2026): stacked slash-skill
invocations load all leading skills (up to 5), not just the first.
- agent/skill_commands.py: split_stacked_skill_commands() consumes leading
/skill tokens (stops at the first non-skill token so slash-path arguments
are never swallowed); build_stacked_skill_invocation_message() composes
the multi-skill turn reusing the existing bundle scaffolding markers so
extract_user_instruction_from_skill_message() keeps memory providers
storing the user's instruction, not N skill bodies.
- cli.py + gateway/run.py: dispatch the stacked path on both surfaces.
- 11 new tests + docs section in skills.md.
- warn once per route instead of on every request (busy senders would
spam the log)
- document X-Webhook-Signature-V2 / X-Webhook-Timestamp in the webhooks
user guide
Follow-ups for salvaged #58461.
Both wired against features the iron-proxy author (@mslipper) confirmed on
PR #30179 — and both verified present in the pinned v0.39.0 source.
Header-auth providers (match_headers):
- New _HEADER_AUTH_PROVIDERS: Anthropic native (x-api-key), Azure OpenAI
(api-key on *.openai.azure.com / *.cognitiveservices / *.services.ai),
Gemini (x-goog-api-key + ?key= query param via match_query).
- TokenMapping grows match_headers + alias_env_names; per-provider header
sets flow into the secrets rules; mappings.json roundtrips them
(legacy files load with the Authorization default).
- GEMINI_API_KEY / GOOGLE_API_KEY collapse into ONE mapping (two
require-rules on the same host would reject each other); the sandbox
gets the token under both names, and the proxy child env mirrors the
alias into the canonical name when only the alias is set.
- Docker backend injects alias env names alongside canonical ones.
- The fail-closed tier is now empty, so fail_on_uncovered_providers and
discover_blocked_providers are deleted (dead toggle otherwise);
_NON_BEARER_PROVIDERS shrinks to genuinely-unswappable signature auth
(AWS SigV4, GCP service-account OAuth) — warn-only, as before.
Management API (hot reload):
- Generated proxy.yaml enables the v0.39 management listener: loopback
only at tunnel_port+2, bearer key from HERMES_IRON_PROXY_MGMT_KEY.
- Key minted at setup (management.token, 0600); start_proxy injects it
(v0.39 refuses to start when api_key_env is empty).
- hermes egress reload -> POST /v1/reload: re-reads proxy.yaml and
atomically swaps the pipeline; 422 leaves the running ruleset
untouched; actionable errors for not-running / pre-management config /
key mismatch. Secrets changes still require restart (daemon env is
read at spawn) — the CLI says so.
Validation: 218/218 unit+CLI+docker tests; 3/3 gated live E2E against the
real v0.39.0 binary (Authorization swap, x-api-key swap, live reload with
token rotation on the same pid). Docs updated.
Five follow-ups to #57659 from post-merge review:
1. install.ps1: gateway scheduled-task re-enable now runs in a finally
(a thrown Remove-Item/uv venv failure previously stranded the user's
gateway autostart disabled), and tasks that were already disabled
before the install are no longer blindly re-enabled.
2. The venv-python holder guard is no longer bypassed by plain --force
(which the desktop bootstrap passes on every update while its lock
probe only checks hermes.exe/app.asar). New explicit --force-venv is
the escape hatch; --force keeps bypassing only the hermes.exe shim
guard.
3. _detect_venv_python_processes now also catches uv/base-interpreter
trampolines whose exe is outside the venv, via cmdline (venv path or
'-m hermes_cli.main' tied to this install root) and cwd.
4. Missing venv python is now UNHEALTHY on managed installs
(.hermes-bootstrap-complete / .update-incomplete markers) so the
repair lane runs instead of 'Already up to date!'; the repair branch
recreates the venv first when it's gone entirely. Dev checkouts keep
reporting healthy.
5. install.ps1 comment no longer claims a Startup-folder disarm the
code doesn't perform (logon-only, not a mid-install respawner).
A single-model Hermes agent never sends temperature; the provider default
applies. MoA hardcoded reference_temperature=0.6 / aggregator_temperature=0.4,
and the coercion float(preset.get(key, 0.6) or 0.6) made unset IMPOSSIBLE to
express: absent, null, empty, and even an explicit 0 all collapsed to the
baked-in default. Every MoA advisor and aggregator therefore ran at 0.6/0.4
while the same model running solo used the provider default — silently
skewing solo-vs-MoA comparisons and overriding provider-tuned defaults.
- moa_config normalization: temperatures coerce to None when absent/blank/
invalid (new _coerce_float_or_none); explicit values incl. 0 honored.
- moa_loop: _preset_temperature() resolves preset values; None flows to
call_llm, which already omits the parameter when None (same contract as
max_tokens). Aggregator still inherits the acting agent's own configured
temperature when the preset doesn't pin one.
- conversation_loop (context-mode MoA): same resolution, no more hardcoded
0.6/0.4 at the call site.
- DEFAULT_CONFIG preset + web_server payload models + docs updated: unset
is the default, pinning stays available.
Group DMs (MPIMs) were classified as DMs and thereby exempted from every
operator control that shared surfaces are supposed to honor: allowed_channels,
require_mention, strict_mention, free_response_channels, and the reaction
guard. Symptom: the bot added 👀/✅ to unmentioned MPIM
messages and still invoked the agent (which then returned NO_REPLY) instead of
the gateway dropping the event before model execution. Removing an MPIM from
allowed_channels did not disable it.
Root cause is the DM classification at adapter.py:
is_dm = channel_type in {"im", "mpim"}
used for BOTH routing exemptions and reaction gating. An MPIM is a shared
surface (multiple humans can see and trigger the bot), not a private 1:1 DM,
so it must be gated like a channel.
This behavior was introduced/reinforced by a trail of Slack group-DM PRs:
- #4633 fix(slack): treat group DMs (mpim) like DMs + reaction guard
- #54632 fix(slack): subscribe to message.mpim + mpim scopes so group DMs work
- #54663 fix(slack): group DMs work OOTB + reinstall nudge
#54632/#54663 correctly made MPIM messages *reachable*; #4633 over-reached by
giving them the DM mention/reaction *exemptions*. This corrects only that
over-reach.
Fix (minimal): introduce `is_one_to_one_dm = channel_type == "im"` and key the
two EXEMPTION sites off it instead of `is_dm`:
- mention/allowlist gating block (`if not is_one_to_one_dm and bot_uid:`)
- reaction guard (`(is_one_to_one_dm or is_mentioned)`)
`is_dm` is intentionally retained for session/thread scoping and chat_type
labeling, where treating an MPIM as a persistent multi-party conversation is
correct — only the mention/reaction exemptions were wrong.
Docs: slack.md now distinguishes 1:1 DMs (mention-exempt) from group DMs
(shared surface; obey require_mention/strict_mention/allowed_channels/
free_response_channels; reactions only when @mentioned).
Tests: +7 in test_slack_mention.py (MPIM unmentioned dropped under
require_mention and strict_mention; MPIM mentioned processed; MPIM off
allowed_channels dropped; MPIM in free_response opted in; 1:1 IM still exempt;
reaction guard drops unmentioned MPIM). Updated _would_process to model the
is_one_to_one_dm gating + strict_mention. 72 passed.
Replace the loopback/PKCE-callback server and manual-paste fallback with
the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The
flow works in headless/SSH/container sessions with no 127.0.0.1 listener,
shrinking the local attack surface.
- Poll the token endpoint with server-provided interval, honoring
slow_down and expires_in; store tokens with auth_mode
oauth_device_code.
- Adaptive proactive refresh skew for short-lived device-code JWTs;
rotated tokens sync back to auth.json, the global root store, and the
credential pool (no refresh-token replay).
- Clear source suppression on successful re-login (CLI + dashboard) and
drop the duplicate dashboard pool entry so exactly one seeded
device_code entry exists.
- Use the shared device_code source name for consistency with the
nous/codex device-code providers.
- Desktop: remove the loopback OAuth flow states and dead type variants;
pkce providers' sign-in URL selection is unchanged.
- Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted
--manual-paste flag from documented commands.
MoA per-turn latency is dominated by advisor GENERATION: turn wall time
correlates ~0.88 with output tokens and ~-0.03 with input tokens (measured over
52 turns). Each turn waits for the slowest advisor to finish writing, and
advisors were uncapped — writing multi-thousand-token essays the aggregator
only needs the gist of.
Add an opt-in per-preset reference_max_tokens knob (mirrors reference_temperature)
that caps ADVISOR output only; the acting aggregator is never capped. Default
None = uncapped, so existing presets are byte-for-byte unchanged (no regression).
Wired through both MoA execution paths (MoAChatCompletions.create and
aggregate_moa_context).
E2E: same task, closed preset uncapped vs reference_max_tokens=600 -> 59s to 33s
(~44% faster), final answer identical/correct.
- hermes_cli/moa_config.py: _coerce_int_or_none helper + reference_max_tokens
in _normalize_preset/_default_preset/flattened view
- agent/moa_loop.py: read preset.reference_max_tokens, pass to reference fan-out
- agent/conversation_loop.py: pass reference_max_tokens on the per-turn path
- tests + docs
The model could pass `toolsets` (top-level and per-task) to delegate_task,
letting it choose which toolsets a subagent got. Toolset selection is a
capability-scoping decision the model should not control; subagents inherit
the parent's enabled toolsets, period.
- Remove `toolsets` from the delegate_task() signature, the registry handler,
the top-level + per-task JSON schema, and the live dispatch path
(run_agent._dispatch_delegate_task — this forwarded it on every model call).
- Single-task and per-task child builds now pass toolsets=None so
_build_child_agent resolves to pure parent inheritance.
- Drop the now-dead _SUBAGENT_TOOLSETS / _TOOLSET_LIST_STR schema-hint block.
- _build_child_agent keeps its internal toolsets param + intersection helpers
(internal API; fed the inherited value only).
- Tests: schema assertions flipped to assertNotIn; added a regression test
proving the dispatch path never forwards a smuggled model `toolsets`.
- Docs: update delegate_task signature refs in the autonomous-ai-agents skill.
Adds Vertex AI as a first-class provider for Gemini models via Vertex's
OpenAI-compatible endpoint. Vertex authenticates with short-lived OAuth2
access tokens (service-account JSON or ADC), not a static API key — the
missing piece behind the recurring requests (#13484, #12639, #56259).
- agent/vertex_adapter.py: OAuth2 token minting + refresh-on-expiry
(5-min margin), ADC->service-account fallback, global vs regional
endpoint URLs. Config precedence: env var > config.yaml > default.
- plugins/model-providers/vertex/: provider profile (auth_type=vertex),
reuses Gemini's extra_body.google.thinking_config translation.
- runtime_provider: vertex short-circuit BEFORE the credential pool so a
credentials-file path is never mistaken for a static API key; mints a
fresh token + computes base_url per resolve.
- run_agent + conversation_loop: _try_refresh_vertex_client_credentials()
re-mints the token and rebuilds the client on a mid-session 401, so a
long-lived gateway agent survives token expiry (~1h).
- auxiliary_client: vertex auth_type branch for side-LLM tasks.
- config.yaml: vertex.project_id / vertex.region (non-secret, bridged to
env); credential path stays in .env (VERTEX_CREDENTIALS_PATH).
- setup wizard + model picker: dedicated _model_flow_vertex; curated
google/gemini-* model list; --provider choices.
- pricing/metadata: Vertex prices off the gemini docs snapshot; endpoint
host auto-maps to the vertex provider (no probe spam).
- lazy_deps + pyproject [vertex] extra: google-auth, opt-in only.
- docs: guides/google-vertex.md + providers page; tests for adapter +
runtime resolution.
Salvages and modernizes #8427 by @slawt onto current main: rewired from
the legacy PROVIDER_REGISTRY path to the provider-profile architecture,
moved non-secret config out of .env into config.yaml, and added the
per-turn 401 token-refresh the original lacked.
Wrap each Telegram initialize() attempt in asyncio.wait_for(HERMES_TELEGRAM_INIT_TIMEOUT,
default 30s). When api.telegram.org and all fallback IPs are unreachable, the connect
chain has no outer bound, so a single initialize() blocks for minutes and the
retry-on-exception loop never fires — the gateway appears to hang after the banner.
The timeout guarantees each attempt is bounded, then retries with backoff, then fails
with an actionable error. Also adds WARNING-level progress logs before DoH discovery
and each connect attempt (visible at default log level).
Salvaged onto plugins/platforms/telegram/adapter.py (Telegram moved from
gateway/platforms/ since the PR was opened). Adds env var to docs + AUTHOR_MAP.
Co-authored-by: Hermes Agent <127238744+teknium1@users.noreply.github.com>
Live DM testing showed a reply to a DM cron brief did NOT continue the job.
Root cause: for a 1:1 DM the governing knob is dm_top_level_threads_as_sessions
(default True), NOT reply_in_thread / cron_continuable_surface. Under the
default, each top-level DM keys to a per-message session (…:dm:<chat>:<ts>),
so a reply mints a new ts and can never converge with the flat …:dm:<chat>
session the cron seed creates.
A 1:1 DM has no thread-vs-timeline split, so "in_channel" has no coherent
meaning for a DM — cron_continuable_surface is a channel concept and is a
no-op for DMs. DM continuation is governed entirely by
dm_top_level_threads_as_sessions:
- false → all top-level DMs share …:dm:<chat> → seed + reply converge → works
- true (default) → per-message sessions → no continuation (cron or interactive)
Option A (chosen): document the requirement; no code change (the flat-DM seed
from the prior commit already lands correctly when the knob is false). Adds a
":::note 1:1 DMs" admonition to cron.md + the zh-Hans mirror.
Verification (real inbound handler, not a hard-coded assumption — the mistake
that made the earlier DM E2E falsely pass): tests/manual/cron_inchannel_dm_e2e.py
drives the REAL _handle_slack_message for a top-level DM under both knob values
and asserts false→converges (…:dm:D_TESTDM == seed), true→diverges
(…:dm:D_TESTDM:<ts>). See decisions.md D9.
Add a per-platform `cron_continuable_surface` extra key
(`thread` default | `in_channel`) so a continuable cron job can deliver
FLAT into a Slack channel — no dedicated thread — and still be
replied-to. In `in_channel` mode the scheduler skips the thread-open
branch (leaves `thread_id=None`); the shipped origin-mirror then seeds
the `(slack, chat_id, None)` shared-channel session — the same bucket
`reply_in_thread: false` routes inbound channel replies to — so a plain
channel reply continues the job in context.
Design: specs/cron-inchannel-continuable (D1–D7, F5). Model B
(shared-channel session), NOT anchoring to the delivery `ts` — on Slack
replying to a specific message IS threading, so a `ts` anchor would only
relocate the thread, never deliver true threadless continuable.
- gateway/platforms/base.py: `supports_inchannel_continuable` capability
flag (default False → unsupported platforms fail SAFE to `thread`).
- plugins/platforms/slack/adapter.py: flag=True; `_cron_continuable_surface()`
resolver (coerces to the two-value enum); `_warn_if_inchannel_without_flat_reply`
connect-time warning (D5: warn, not hard-require — the misconfig fails safe).
- gateway/config.py: shared-key bridge line (top-level OR nested config).
- cron/scheduler.py: read the key generically from platform config, gate
the `in_channel` branch on the adapter capability flag, skip thread-open.
No new seed function (reuses the existing mirror — G6).
Pairing (docs): `in_channel` + `reply_in_thread: false` +
`require_mention: false` (or a free-response channel). Missing
`reply_in_thread: false` fails safe to a threaded continuation.
Gateway-side config flag — `/restart` to apply; NO Slack app reinstall.
Tests (from inside the worktree, PYTHONPATH=$PWD):
- +6 cron scheduler tests (in_channel skips thread-open; seeds flat
channel session with thread_id=None; thread-mode regression;
fail-safe on unsupported platform; value coercion). Prove-fail:
removing the `and not in_channel_surface` guard turns the two
load-bearing tests RED; restore → GREEN.
- +10 slack resolver/capability/warning tests; +2 config-bridge tests.
- tests/manual/cron_inchannel_e2e.py: offline E2E driving BOTH real
legs (delivery seed + inbound reply keying) → both converge on
(slack, C, None).
- No regressions: test_slack.py 216 passed alone; broader sweep green
(4 pre-existing cross-file-ordering failures reproduce identically on
pristine origin/main).
Docs: cron.md + slack.md + zh-Hans mirrors of both.
The messaging gateway table still listed /new ("Start a new
conversation") and /reset ("Reset conversation history") as two
separate commands with divergent descriptions. /reset is an alias
of /new (see COMMAND_REGISTRY in hermes_cli/commands.py) — same
handler, fresh session ID + history. Collapse them into one row
matching the registry wording and the CLI table already on line 39.
Closes#42829.
Remove scripts/setup_open_webui.sh and its 'one-command local bootstrap'
doc sections (EN + zh-Hans). The script pip-installed the third-party Open
WebUI frontend into ~/.local and managed a launchd/systemd user service —
a maintenance liability for downstream software we don't own, and the source
of the LAN first-admin signup footgun in #36121.
The Open WebUI *integration* via the OpenAI-compatible API server is
unaffected: the Docker/Docker-Compose setup, multi-user profile guide, and
troubleshooting in open-webui.md stay, and Open WebUI remains a listed
supported frontend. Only the install-and-service bootstrapper is gone.
@janrenz's PR #35862 added prompt_caching.enabled=false at init only. But
_anthropic_prompt_cache_policy re-derives _use_prompt_caching on every /model
switch (agent_runtime_helpers) and fallback-model swap (chat_completion_helpers),
which re-enabled markers and re-broke the strict proxy the toggle was meant to fix.
Move the kill switch into anthropic_prompt_cache_policy so it returns (False, False)
on every path. Drop the now-redundant init-time override (kept @janrenz's isinstance
hardening on the cache_ttl read). Add policy-level tests + docs for the toggle.
Follow-up to salvaged PR #35862.
Replace the interim monospace table fallback with Slack's native `table`
block (rows of rich_text cells). Addresses the core ask in #18918.
- _table_block(): builds type:"table" with rich_text cells, so inline
formatting (bold, links, code) renders inside cells.
- Column alignment parsed from the markdown separator row (:---, :-:, --:)
into column_settings (left = default/null-skip, center/right emitted).
- Escaped pipes (\\|) are not treated as column separators.
- Respects Slack's table limits (100 rows / 20 cols / 10k aggregate chars);
oversized or unparseable tables gracefully fall back to aligned monospace
(rich_text_preformatted), so a big table never breaks the message.
Docs (EN + zh-Hans) updated to describe native tables + the fallback.
Tests: native table shape, alignment->column_settings, inline-formatted
cells, oversized/too-wide monospace fallback, escaped-pipe cell. Prove-
failed against a stubbed _table_block (native-table tests fail, fallback
tests stay green). All existing Slack tests still pass.