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).