Qwen3.8 Max is live on both OpenRouter and the Nous portal
(qwen/qwen3.8-max, 1M context, 131K max output). Per the
newest-max-replaces-last-max convention, it takes qwen3.7-max's slot
in both curated lists.
- hermes_cli/models.py: OPENROUTER_MODELS + _PROVIDER_MODELS[nous]
swap qwen/qwen3.7-max -> qwen/qwen3.8-max
- agent/model_metadata.py: DEFAULT_CONTEXT_LENGTHS entry for
qwen3.8-max at 1,000,000 (verified against OpenRouter live
metadata and Nous /v1/models 2026-08-03)
- tests/test_empty_model_fallback.py: swap incidental catalog fixture
to the surviving slug
- website/static/api/model-catalog.json: regenerated
Pricing snapshot skipped: both routes bill via official_models_api
(live pricing), verified with resolve_billing_route. Reasoning
timeout floor already covered by the qwen3 prefix (180s).
The reset keywords have existed in both CLI and gateway handlers since
June but were undocumented — users couldn't find how to cancel a
personality overlay. Adds a 'Resetting to the default' section to the
personality feature page and mentions the reset in the CLI guide,
slash-command reference (both tables), and messaging command table.
restore_primary_runtime retries the primary every turn once the 60s
transient cooldown clears. For subscription-window limits (Claude
Pro/Max 5h windows, Codex weekly caps) the reset is hours or days away,
so every retry is a guaranteed failure costing two provider switches
and two prompt-cache invalidations per turn.
Add CredentialPool.next_available_at() (earliest reset across exhausted
entries; None when available now or no reset info) and gate the restore
on it: skip while the primary's pool says nobody can serve, restore on
the first turn after the reset elapses. Fail-open: any gate error or
missing reset info falls through to the existing per-turn retry, so
recovery can never be later than today. Cross-provider fallbacks
consult the PRIMARY's pool (not the attached fallback pool), reusing
the loaded pool for the existing rebind to keep auth reads at one per
restore.
The runtime footer (`/footer`) shows what model ran and how full the context
is, but not how long the turn took. On a messaging platform there is no
progress bar and no shell timer — a turn that took 4 seconds and one that took
four minutes produce visually identical replies. Users comparing models,
providers, or reasoning levels have no at-a-glance signal for the one
dimension they most often care about, and "was that slow or did I imagine it?"
is unanswerable after the fact.
Adds a `latency` field to the existing footer machinery, rendering the
wall-clock duration of the agent run: `<1s`, `22s`, `1m05s`.
`gateway/run.py` measures with `time.monotonic()` immediately around the
`self._run_agent(...)` await in `_handle_message_with_agent` — the same
function that already builds the footer, so the value is the user-perceived
turn duration (monotonic, so it is immune to wall-clock/NTP adjustment).
`latency` is deliberately NOT in `_DEFAULT_FIELDS`. It is opt-in via
`display.runtime_footer.fields`. Every existing footer — and every footer a
user has today without touching config — renders byte-identically.
This is enforced by tests, not just asserted:
- `test_latency_not_in_default_fields` pins the default tuple.
- `test_resolve_footer_config_default_fields_exclude_latency` pins what
config resolution produces for an untouched config.
- `test_default_footer_renders_byte_identically` pins five exact output
strings for default-config renders **while supplying `turn_seconds`** —
proving that even when the caller measures timing, a default-configured
footer does not show it.
- `test_default_build_footer_line_ignores_turn_seconds` asserts
`build_footer_line(...) == build_footer_line(..., turn_seconds=125.0)`
under default fields.
Adding `latency` to `_DEFAULT_FIELDS` fails 11 of these tests.
No new config surface (reuses `display.runtime_footer.fields`), no new env
vars, no new core tool, no new model-facing schema. One new module-private
helper (`_format_latency`), one new keyword argument threaded through the two
existing footer functions, and 3 lines in `gateway/run.py`.
`turn_seconds` defaults to `None` and the field is skipped when it is `None`
or negative, so any call site that does not measure timing keeps working
unchanged.
`tests/gateway/test_runtime_footer.py` (+185): `_format_latency` boundary
table (sub-second, rounding at 59.4/59.6, the `m{:02d}s` zero-pad, 60m), the
render/skip/opt-in matrix, field-order placement, `build_footer_line`
threading, and the byte-stability block above.
RED-proved by mutation — each of these breaks tests:
- `latency` added to `_DEFAULT_FIELDS` → 11 failures
- dropping the `turn_seconds is not None and >= 0` guard → 2 failures
- `{sec:02d}` → `{sec}` → 6 failures
- `build_footer_line` not threading `turn_seconds` → 1 failure
51 passed in `tests/gateway/test_runtime_footer.py`; 54 passed across the
footer blast radius. `ruff check` clean.
The top-level delegate_task description repeated content the model already
receives through parameter descriptions: the concurrency limit (tasks param),
the full nesting clause (role param), context-passing guidance (goal/context
params), and background semantics (background param). Every API call paid for
the duplication (~4,000 chars).
The description now carries only what exists nowhere else in the schema:
use/don't-use routing (execute_code, cronjob), the no-poll rule, the
non-durability warning, the self-report verification contract with concrete
verbs, the language-passing example, the leaf blocked-tool list, and model
inheritance. 3,963 -> 1,704 chars (~570 tokens saved per API call), and the
top-level text is now static (dynamic limits flow only through the two param
descriptions, which are already rebuilt per get_definitions() call).
A/B benchmark across 4 models (gpt-4o, gpt-4o-mini, claude-haiku-4.5,
llama-3.3-70b) showed the naive compaction in PR #72813 regressed weaker
models on exactly the passages it cut (side-effect verification 8/8->0/8 on
gpt-4o-mini; language passing 3/3->0/3 on haiku-4.5). This version keeps
those benchmark-sensitive hooks verbatim.
Tests pin the contracts at keyword level (not prose-literal) plus a size
ceiling, and verify dynamic limits still reach the model via the tasks/role
param descriptions.
Refs #72737, supersedes the delegate_task half of PR #72813.
Live-run findings from a real fact-checking task (ankylosing spondylitis
genetics, 7 authoritative sources) against the new mode:
- Verbatim check rejected a legitimate quote because web_extract returns
markdown: the MedlinePlus sentence is "including _[ERAP1](https://...)_,
_[IL1A](...)_" on the wire but plain prose to a reader. The agent was
forced onto a weaker evidence fragment — the opposite of the point.
Matching now canonicalizes inline links to their label and drops
emphasis/code markers and backslash escapes on both sides, so quoting
the sentence a reader sees works. Paraphrases are still rejected.
- Escaped asterisks (HLA-B\*27) no longer have to be reproduced in the
quote, so extractor artifacts stop leaking into rendered evidence.
- New `render --replace-in <draft>`: rewrites a draft's Sources block in
place, idempotently. Previously the only path was hand-slicing the
file, which also tripped over the emitted heading being `## Sources`
while the prose said "Sources:".
- verify stats: report the provenance total that the percentage is
actually computed from (cited + [unverified], counted once), and print
the line as `info:` instead of `warn:` when nothing is wrong. The old
line printed 17 cited / 2 unverified next to 72%, which does not
reconcile — a sentence can be both.
- SKILL.md documents the emitted heading, --replace-in, and exactly what
counts as a prose sentence for --min-coverage.
7 new tests (47 total) using the real MedlinePlus/Frontiers markup;
6 sabotage runs, all red.
Extends the citation ledger with evidence-backed fact-checking:
- New `quote` subcommand attaches verbatim supporting quotes to a
source; the quote is rejected unless it appears verbatim
(whitespace/case-insensitive) in the fetched page text, so a
paraphrase or misremembered figure cannot masquerade as evidence.
- `verify --evidence` fails a draft whose cited sources carry no
attached quote.
- `render --style evidence` prints each source's quotes beneath its
URL, showing the claim -> source -> exact-text chain.
- `[unverified]` marker declares model-knowledge claims; counts toward
--min-coverage so provenance is declared for every sentence without
forcing fake citations.
- SKILL.md: new Fact-Checking Mode section + pitfalls; version 1.1.0.
- 10 new tests (40 total), all proven live by sabotage runs.
Covers the fact-checking/evidence-transparency half of #28289.
- website/docs/user-guide/configuration.md (en) and the zh-Hans
translation gain a 'Session Stall Watchdog' section: default 300,
0=disabled, notify-only semantics (never kills the turn — contrast
gateway_timeout), one notification per stall episode, and the exact
stall message text so it is greppable.
- cli-config.yaml.example: the two in-agent compression timeout keys
(compression.context_timeout_seconds /
compression.context_total_ceiling_seconds) are shown as commented
lines next to session_stall_timeout's example for discoverability.
The pooled worker closure captured the caller's live `messages` list and
compress_context explicitly supports plugin/legacy context engines that
mutate that list in place — so after a host timeout, a late engine could
rewrite the live conversation (roles, ordering, persisted content)
concurrently with the resumed turn.
The worker now deep-snapshots the transcript on the worker thread before
any engine code runs; the caller's list object is never handed to pooled
code. Results reach caller-visible state only through the returned value
of an ADMITTED commit (the host discards results on timeout/cancel), and
durable SessionDB mutation was already gated behind the commit fence.
No-op passes map the unchanged snapshot back to the caller's original
list so identity-based no-op detection and flush dedup keep working.
Document the thread-safety contract for context-engine and
memory-provider extension points (they now run on pooled threads) in the
module docstring and the context-engine plugin guide.
Regression: an in-place-mutating engine plus host timeout proves the
caller's live transcript is byte-identical WHILE the worker is still
blocked inside the engine (released only after the assertions).
PR #76354 review, blocking finding 3 / merge gate 3.
The post-begin_commit() waiter previously called unbounded future.result(),
so the advertised compression.context_total_ceiling_seconds was silently
unenforced for commit-phase hangs. The commit still must complete (abandoning
an in-flight SessionDB mutation would diverge live messages from durable
state), but the wait is now bounded in increments against the remaining
ceiling: on ceiling breach the overrun is logged (WARNING escalating to
ERROR), surfaced once through the user-visible warning channel via the new
on_commit_overrun callback (wired to _emit_warning in run_agent.py), and the
host keeps waiting in bounded slices until the commit finishes.
Documented guarantee (config comment + docs, en/zh): summary phase bounded
by the ceiling; commit phase logged + surfaced if it exceeds it — never
silently hung, never abandoned mid-commit.
Test updated to assert the surfacing fires (previously accepted a silent
over-ceiling wait); adds coverage that a raising overrun callback cannot
break the commit wait.
Once begin_commit() wins, SessionDB mutation cannot be fence-cancelled;
document that context_total_ceiling_seconds covers the summary phase only
and pin the hang-wait contract in tests.
Three mechanisms to detect and notify when gateway sessions stall silently:
1. Mid-turn activity heartbeats stamped to SessionDB so hermes sessions list
and hermes status show progress during long turns without new message rows.
2. Stall watchdog: when a busy session has pending inbound and the shared
activity clock is idle past agent.session_stall_timeout (default 300),
log a WARNING and notify the user once to try /new. Notify-only; does
not kill the turn.
3. Compaction timeout: fenceless compress_context callers get a progress-aware
host budget (compression.context_timeout_seconds default 120 idle,
compression.context_total_ceiling_seconds default 600 ceiling). On timeout,
cancel via commit fence, skip compaction without dropping messages, and
continue the turn.
Closes#72016 (slices 1-3; slice 4 cumulative SSE stream-retry deadline
remains a follow-up).
Cherry-picked from PR #72424 by @fangliquanflq.
- New website/docs/user-guide/messaging/a2a.md: when/where to use A2A
(cross-machine, specialist peers, being callable) vs delegation/kanban
for same-machine multi-agent; enable, outbound tools, inbound surface,
security model, env reference, quick test, troubleshooting. Registered
in sidebars.ts and the messaging index.
- README/DESIGN/plugin.yaml/protocol.py prose updated to name the A2A
v1.0 canonical discovery path /.well-known/agent-card.json (the code
already served both; only the docs lagged).
Builds on the adapter list_channels() hook (cherry-picked from #43545 by
@Guoen0):
- plugins/platforms/simplex: implement list_channels() — enumerates
contacts (/contacts) and groups (/groups) over the live daemon
WebSocket into the channel directory. Returns None when the WS is
down so the directory falls back to session discovery instead of
wiping known targets.
- hermes send --list: merge configured-but-undiscovered platforms into
the listing. Previously a platform configured only via env (e.g. a
fresh SimpleX setup used for outbound sends) was silently omitted,
leaving users guessing at platform names.
- format_directory_for_display(): accept an explicit platforms view and
render empty platforms with a targeting hint instead of hiding them.
- docs: simplex hermes-send section.
Reported by Fedpostoffice on Discord (simplex missing from
hermes send --list; guessed platform names simplex-chat/simplex-relay).
- delivery_id is now generated once per firing and used for both the
X-Hermes-Delivery header and the signed body's delivery_id field —
previously they were two different uuid4s, breaking receiver-side
dedupe as documented.
- 3xx responses are no longer followed: urllib's default redirect
handler converts a redirected POST into a body-less GET, silently
dropping the signed payload. Redirects now log a misconfiguration
warning and count as delivery failure (no retry).
- Docs: receiver-side replay-protection guidance (dedupe on
delivery_id, timestamp freshness window) + redirect semantics.
- Tests: 5xx retry count, redirect-not-followed (sabotage-verified),
header/body delivery_id equality.
The inverse of the inbound webhook platform: hooks.outbound in
config.yaml lists HTTP targets + the plugin-hook events they subscribe
to (on_session_end, subagent_stop, post_tool_call, ...). Each firing
POSTs a JSON payload (same top-level shape as shell hooks' stdin wire)
signed GitHub-style with HMAC-SHA256 (X-Hermes-Signature-256).
Rides the existing hook bus — notify-only callbacks registered on the
plugin manager at the same CLI/gateway/main entry points as shell
hooks. Delivery is fire-and-forget via a bounded queue + single daemon
worker thread, so a dead endpoint can never stall a tool call. Bounded
retries (5xx/conn errors once; 4xx never). secret_env preferred over
inline secret. HERMES_SAFE_MODE skips registration. hermes hooks list
shows outbound targets with signed/UNSIGNED status.
Zero new model tools, zero new subsystems.
Answers and written deliverables that rest on retrieved information now get
inline numbered citations plus a mechanically-rendered Sources list, with a
persistent ledger that makes a hallucinated citation detectable.
- skills/research/grounded-citations/scripts/sources.py: stdlib citation
ledger (add/ingest/list/render/verify) at
$HERMES_HOME/cache/citations/ledger.json, profile-aware, O_EXCL-locked so
parallel subagents sharing a ledger can't collide on ids
- SKILL.md: cite-while-drafting procedure, register-at-retrieval rule,
pitfalls, verification gate
- references/citation-formats.md: per-target placement (markdown, LaTeX/PDF,
docx, pptx, xlsx, wiki, BibTeX handoff to research-paper-writing)
- references/grounding-rationale.md: why numbered ids (ALCE 2305.14627,
WebGPT 2112.09332, Perplexity marker conventions), and how this relates to
the in-process registry in PR #44833
- tests/skills/test_grounded_citations_skill.py: 30 tests
Every ORDER BY id query on the messages table sorted or scanned the
whole session: get_messages_around's window seek, latest_message_row_id
(LIMIT 1), and get_messages' full-load ordering all paid O(session
history) per call — hot mid-turn via session_search and reactions.
messages.id is an original column (INTEGER PRIMARY KEY AUTOINCREMENT),
so the index lives in SCHEMA_SQL next to idx_messages_session — no
legacy-column migration hazard (the kanban lesson from #28776 does not
apply).
Measured (real schema, one 20k-message session, median of 30):
get_messages_around 7.08 -> 0.22 ms (32x), latest_message_row_id
3.37 -> 0.011 ms (307x), get_messages full load 111.6 -> 98.6 ms
(1.13x — remaining cost is row deserialization, not the sort).
Window results byte-identical at probe points across the session.
Tests: VM-step pin (get_messages_around bounded work, calibrated
~12 vs ~855 handler calls, threshold 300 — fails without the index)
and window parity with/without the index. No EXPLAIN/plan text
(behavior contracts, AGENTS.md).
#76499 landed the npm floor as >=11.17.0 on a Node >=20 baseline. This
branch takes the other half of the same constraint: the vendored Node 26
tree now installs npm 12 into itself, so the toolchain satisfies the
stricter floor rather than the manifest relaxing to meet the tarball.
Resolved package.json + package-lock.json to node >=26.0.0 / npm >=12.0.0
and refreshed npm_engine.py's illustrative range to match. website/'s
mirror keeps #76499's >=11.17.0 — it is not a root workspace and builds
on its own Node.
Hermes now pins its toolchain to Node 26 everywhere. Every path that
installs, accepts, heals, or upgrades a Node runtime moves from the old
22-default / `^20.19 || >=22.12` floor to a single rule: Node >=26.
Installers:
- scripts/install.sh — NODE_VERSION=26; node_satisfies_build() collapses
the two-branch Vite floor to `major >= 26`; user-facing messages updated.
- scripts/install.ps1 — $NodeVersion=26; Test-NodeVersionOk likewise;
winget fallback switches OpenJS.NodeJS.LTS -> OpenJS.NodeJS (26 is
Current, not LTS — the LTS manifest would reinstall a too-old Node).
- Dockerfile — node_source stage node:22-bookworm-slim -> node:26 (digest
pinned, amd64 sha256:9e6f...bf73).
- nix/ was already on nodejs_26 (lib.nix, npm-12-0-2.nix); the checks.nix
wrapper check ratchets from `>= 20` to `>= 26`.
Heal/upgrade paths:
- scripts/lib/node-bootstrap.sh — HERMES_NODE_TARGET_MAJOR default 22->26
and HERMES_NODE_MIN_VERSION default 20->26, so heal_managed_node,
_nb_install_bundled_node, and the fnm/proto/nvm/brew rungs all target 26
and stop accepting an on-PATH Node below it. Both remain env-overridable.
- hermes_constants.py — _HERMES_NODE_TARGET_MAJOR fallback 22->26, which
drives the Windows heal path's latest-v26.x download.
Version gates:
- package.json engines.node >=20 -> >=26; apps/desktop engines
`^20.19.0 || >=22.12.0` -> `>=26.0.0`.
- CI setup-node: all five workflows 22 -> 26.
- Docs describing Hermes's own toolchain updated (windows-native, docker,
acp, nix-setup, contributing). Skill docs describing third-party tools'
own requirements are untouched.
Termux still installs via `pkg install nodejs` best-effort (nodejs.org
ships no Android tarballs); that path was never version-gated.
Verified: bash -n on both shell scripts, PowerShell AST parse of
install.ps1, latest-v26.x index resolves (node-v26.5.1), and the install
test suite — 18 tests across the 5 install/runtime test files — passes.
Users following abbreviated links guess /docs/quickstart and
/docs/installation and hit raw GitHub-Pages 404s — the real pages live
under /docs/getting-started/. Add client redirects for both.
Consumer-onboarding audit finding #1, Aug 2026.
Follow-ups from sweeper review of #43763:
- tests/docker/test_tini_compat_shim.py asserts the dispatcher
ENTRYPOINT (with /init delegation check) instead of a bare /init
- tests/docker/test_smoke.py gains a docker run --init regression
for the non-PID-1 fallback (#38349)
- website/docs/user-guide/docker.md and the s6 supervision skill
document the dispatcher and its wrapped-runtime fallback
Google API and authentication packages permit vulnerable httplib2 and pyasn1
transitives, while the Workspace and Google Chat runtime installers previously
treated any importable version as sufficient. Existing environments could
therefore remain vulnerable after the project dependency pins were repaired.
Carry the fixed versions through the Google and Vertex extras, lazy feature
requirements, lockfile, and both runtime installers. Route the documented
Google Chat installation path through its maintained secure requirements
instead of an unconstrained direct pip command.
Detect stale distributions, install only unsatisfied requirements, and verify
the result before continuing. Behavioral tests cover those repair invariants
without freezing manifests, lockfiles, or complete package sets.
Related #72108
Extracted from #72840
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Follows PR #75592: the notifier is no longer gated on
kanban.dispatch_in_gateway. Every gateway delivers events for
subscriptions owned by the profiles whose adapters it hosts; legacy
unstamped subscriptions go only through the confirmed dispatcher
lock owner. Adds a 'Multi-profile setups' subsection to the kanban
Gateway notifications docs (en + zh-Hans).
Two fixes for the Skills Hub "View source" links on ClawHub skills:
1. Source URL generation was missing the required {owner} segment —
https://clawhub.ai/skills/{slug} → 404. Correct format is
https://clawhub.ai/{owner}/skills/{slug}. When the owner handle is
unavailable, source_url is now "" (card omits the button) instead of
emitting a broken link.
2. _fetch_owner_handle() previously delegated to _get_json() which
returned None on any non-200 response with no retry. Under HTTP 429
rate-limiting the "50 consecutive failures" safety rail in
enrich_owners() fired immediately — the documented claim "Respects
HTTP 429 rate-limit responses with exponential backoff" was not
actually implemented. Now has its own retry loop: 3 attempts, honours
Retry-After on 429, exponential backoff on 5xx/transport errors, no
retry on 4xx.
Changes:
- tools/skills_hub.py: _coerce_skill_payload carries owner from top-level
response; inspect() captures owner from detail API; _fetch_owner_handle()
added with bounded retry/backoff; enrich_owners() batch method with
safety rails (30 workers, early termination at 50 consecutive failures).
- website/scripts/extract-skills.py: _source_url() reads extra["owner"]
for ClawHub.
- scripts/build_skills_index.py: batch enrichment step after crawling.
- tests: 35 URL/enrichment tests + 7 retry tests (42 total).
Signed-off-by: dongjiang <dongjiang1989@126.com>
Finishes the input stash started in the preceding commit from PR #4771.
That PR shipped only the panel renderer: its `@kb.add('c-s')` handler and
stash-state initialization were lost in a rebase, so the panel predicate
read undefined `_stash_panel_open` / `_stash_list` and the feature was
unreachable. This adds the missing half and the tests the PR never had.
Resolves the review feedback on #4771:
- Rebuilt the stash on current main's keybinding setup. The `c-s` key was
unbound repo-wide, so there is no conflict.
- Extracted the state machine into `hermes_cli/prompt_stash.py` as pure
functions (no prompt_toolkit import) so it is directly unit testable —
the PR was cli.py-only with zero tests.
- Dropped the PR's unrelated changes: delegation `supervisor_model` /
`execution_model` config aliases, and stale reverts of the banner
builder, worktree pruning, logging setup, and MCP toolset validation
that its 14k-commit-old base dragged along.
- Fixed the 📌 double-width measurement for real. Three commits in the PR
("subtract 1 from len()", "use bare len()", "subtract 1 again") were
chasing this by tweaking `len()`; all horizontal math now goes through
`_status_bar_display_width` (prompt_toolkit `get_cwidth`), which also
keeps CJK previews inside the border. Narrow terminals fall back to
compact header/footer labels instead of overflowing — caught by a
parametrized width test, not by eyeballing.
Gesture (the contributor's design, kept):
- Composer has content → push onto the stash, clear the input.
- Composer empty, one stashed → pop it straight back.
- Composer empty, 2+ stashed → open the browse panel (↑↓ / Enter / D / Esc).
- Panel open → Ctrl+S closes it.
Pushing onto a stack rather than a single slot is what makes repeated
Ctrl+S safe: a second stash never silently overwrites the first, and with
2+ parked the panel asks rather than guessing which to restore. A `📌 N`
status-bar badge and a composer placeholder advertise the parked draft so
it cannot be silently forgotten.
Deliberate departures from the PR:
- No auto-restore after the agent responds, and no `display.stash_auto_restore`
config key. The PR itself had already defaulted this to false as
"avoids surprising the user"; a keystroke the user pressed should not
cause text to reappear on its own, so the dead default is dropped
rather than carried as config surface.
- Nothing is persisted to disk. Drafts routinely contain pasted
credentials and NDA material, so the stash is session-scoped and
in-memory only. Any future persistence must route through
`get_hermes_home()`.
- Suppressed while a modal prompt owns the composer (sudo / secret /
approval / clarify / slash-confirm / model picker) so Ctrl+S can never
stash a password.
- Restoring images extends `_attached_images` instead of replacing it, so
an attachment added since the stash was taken is not silently dropped.
- `buf.reset()` on stash (not `text = ""`) clears completion state,
selection, and undo stack with the text.
Tests: 95 new tests across two files — 66 on the state machine (empty
buffer is a no-op, exact round-trips including newlines/tabs/CJK/fences,
no-clobber ordering, cap eviction, indicator states, panel cursor
clamping and deletion, the full resolve_ctrl_s decision table) and 29 on
the cli.py wiring (per-instance stash, keybinding registration guard,
layout slot, panel bounded at 8 widths, status-bar indicator lifecycle).
The keybinding-registration test asserts the `c-s` handler exists in
source specifically so the rebase loss that broke #4771 cannot recur.
Verified: 153 passed, 0 failed across the two new files plus
tests/cli/test_cli_init.py and tests/cli/test_cli_extension_hooks.py.
ruff check clean; check-windows-footguns clean.
Docs: Ctrl+S added to the CLI keybindings table.
Co-authored-by: CK iRonin.IT <cyprian@ironin.pl>
Dated snapshot of deepseek-v4-flash, live on both provider endpoints
(verified via OpenRouter /api/v1/models and Nous portal /v1/models).
Context (1M via deepseek-v4-flash prefix match), reasoning stale-timeout
floor (600s), and pricing (official_models_api live billing on both
routes) all resolve without new entries. model-catalog.json regenerated
via scripts/build_model_catalog.py.
The test runner moved to per-file subprocess isolation via
scripts/run_tests_parallel.py (hermetic `env -i`, worker count auto-scaled
from CPU count, FLAKY-retry policy) — no pytest-xdist, no SIGALRM per-test
timeout fixture. Docs still described the old runner in many places:
- AGENTS.md: "-n auto xdist workers, in-tree subprocess-isolation plugin"
clause replaced with the current per-file-subprocess description; the
`::test_x` single-test example now shows file + -k (runner is
file-granular).
- CONTRIBUTING.md: "hermetic env, 4 xdist workers" comment corrected;
`tests/conftest.py::_enforce_test_timeout` reference redirected to the
win32 timeout-method shim in `tests/conftest.py::pytest_configure`.
- skills/autonomous-ai-agents/hermes-agent/references/contributor-guide.md
and windows-quirks.md: same corrections (the bundled skill mirrors the
contributor docs); Windows workaround no longer installs pytest-xdist
or passes -n 0.
- website/docs + zh-Hans i18n mirrors: same fixes in adding-providers.md
and the bundled-skill doc pages.
- skills/software-development/python-debugpy/SKILL.md (+ zh-Hans mirror):
"-p no:xdist"/"-n 0" pdb advice rewritten for the captured per-file
subprocess runner.
- skills/creative/comfyui/tests/README.md: parent-repo "-n auto by
default" rationale updated to past tense.
Combined salvage of PR #38295 (konsisumer), PR #51354 (TutkuEroglu,
redirected to the current conftest truth and the relocated
references/contributor-guide.md), and PR #54956 (waroffchange).
Co-authored-by: TutkuEroglu <rrandqua@gmail.com>
Co-authored-by: waroffchange <116298975+waroffchange@users.noreply.github.com>
Buzz Desktop v0.5.1 now renders Hermes' ACP model menu in agent runtime
settings. Add a short note under the Buzz Desktop host section explaining
where the list comes from (the shared authenticated-provider inventory),
the provider:model / custom:<name>:<model> ID shapes, and that a pick is
session-scoped rather than a Hermes-wide default change.
Replaces the half-duplex per-playback barge monitors with ONE listener
that runs for the entire agent turn in continuous voice mode: armed at
utterance-submit, disarmed when the turn is fully done (response + TTS
finished). Fixes Teknium's live report that voice interruption never
works: (a) not while the LLM is generating, (b) not while TTS plays.
Root causes:
- HALF-DUPLEX GAP: the barge monitor only spawned when TTS playback
STARTED (cli.py streaming/whole-file paths, gateway _tts_stream_begin).
During LLM generation there was NO microphone listener at all.
- PLAYBACK DEAFNESS: the monitor calibrated its VAD noise floor WHILE the
speaker was blasting TTS (speaker bleed baked into the floor), then
multiplied it by 8x with a 1s strictly-consecutive block requirement —
normal speech could rarely reach the trigger, and the 2s grace
swallowed early interjections.
New model — tools/voice_mode.full_duplex_listen():
- Pre-playback calibration: quiet-room noise floor established at turn
start and HELD through playback (never recalibrated against bleed).
- Phase-aware trigger: generation = floor x voice.barge_in_threshold_multiplier
(new config, default 3.0, justified by synthetic-frame tests);
playback = additionally clamped to a 1500-RMS minimum so bleed alone
can't trip; 4000-RMS ceiling keeps speech always reachable.
- Windowed-majority detection (>=80% of a 300ms window) instead of the
strictly-consecutive counter that reset on intra-word energy dips.
- Grace on playback ONSET only (voice.barge_in_grace_seconds, default
down 2.0 -> 0.5) — suppresses the onset transient, not the mic.
- Debug diagnostics at every decision point (calibrated floor, per-window
RMS above 50% of trigger, trip/no-trip, grace suppressions) — always
logger.debug, mirrored to stderr under HERMES_VOICE_DEBUG=1.
Phase behavior (CLI cli.py + tui_gateway/server.py, same model):
- generation: speech interrupts the in-flight turn via the SAME seam the
typed/Ctrl+C interrupt uses (agent.interrupt()), cuts any pending TTS
pipeline so the stale reply never plays, and submits the captured
interjection (pre-roll capture, first syllable kept) as the next turn.
- playback: cuts TTS (streaming pipeline stop + fallback speak stop
events + file player) and submits the capture.
- stop phrase honored in BOTH phases: mid-generation 'stop' interrupts
the turn AND ends the voice chat (stop everything).
- one listener instance spans generation -> playback (no re-arm race);
double-arm refused (CLI _voice_fd_active / gateway _fd_listener_active).
Gateway specifics: _arm_full_duplex_listener() at _run_prompt_submit turn
start and inside _tts_stream_begin; _speak_text_with_barge registers its
(stop, done) pair in _fd_speak_pipelines so fallback speaks are cut and
tracked; _tts_stream_barge_in_monitor kept as a shim that arms the new
listener. Desktop renderer owns its own mic path (voice-barge-in.ts) and
is unaffected; if desktop backend-mic mode is used it inherits via the
gateway.
Tests: full_duplex_listen synthetic-RMS suite (speech-over-bleed trips,
bleed alone doesn't, quiet floor held through playback, grace window,
multiplier math 3x vs 8x, windowed-majority dips), CLI listener phase
tests (generation interrupt seam, playback cut, lifecycle spans phases,
double-arm, config forwarding, stop-phrase-mid-generation), gateway
generation-phase interrupt + stop-phrase tests. Generation-interrupt
test sabotage-verified.
Fixes#67278. Since config v12 (see #8776), custom_providers: list is
legacy — the migration converts it to the providers: dict and the
resolver reads the dict first (runtime_provider.py). Docs still taught
the legacy list as primary.
- integrations/providers.md: all 8 YAML examples converted to
providers: dict (field mapping code-verified: api, default_model,
transport), one consolidated legacy-format note
- configuring-models.md, configuration.md, credential-pools.md,
migrate-from-openclaw.md, provider-runtime.md, faq.md: examples and
prose flipped to dict-first; legacy list noted as still-read
- adding-providers.md untouched (sole mention is a literal test
filename)