Commit graph

4167 commits

Author SHA1 Message Date
Flownium
774d92fbfa fix: approve listed pairing requests 2026-07-29 17:49:33 -05:00
teknium1
3c6e7b1b11 fix(config): don't share the cached models mapping with normalized entries
Companion to the no-mutate fix: normalized['models'] retained a
reference to the caller's (possibly cached) models dict, and the
normalized entry escapes into long-lived runtime state
(agent._custom_providers). Shallow-copy so runtime writes can never
reach the shared config cache.
2026-07-29 15:28:15 -07:00
golldyck
bfe4cbdc2a fix(config): stop _normalize_custom_provider_entry mutating the caller's dict
The normalizer writes alias keys into the entry it is given
(entry['key_env'] = entry['api_key_env'] and entry[snake] =
entry[camel]) while building its normalized copy. Two of its three
callers — get_compatible_custom_providers and
providers_dict_to_custom_providers — pass live sub-dicts straight
from load_config_readonly()'s shared cache (only
_custom_provider_entry_to_provider_config defends with dict(entry)).

A config written with the documented camelCase / api_key_env aliases
therefore gets its cached copy polluted with injected duplicate keys,
violating the cache's explicit no-mutation contract; every later
load_config() deepcopy inherits the duplicates, and any
save_config(load_config()) flow (setup wizard, dashboard writes,
model persist) writes them back to config.yaml. The aux-client TLS
resolution runs this on every auxiliary client build, so the
mutation also happens unlocked on worker threads against a shared
object.

Shallow-copy the entry up front; the function's return value is a
separately-built dict, so behavior is otherwise unchanged.
2026-07-29 15:28:15 -07:00
Teknium
1a088989bc
Merge pull request #66730 from NousResearch/feat/hsp-sync-client
feat(sync): HSP/1 personal skill sync client (M1 client)
2026-07-29 15:20:35 -07:00
Gille
94d1dff50d fix(wake): route desktop control and select input devices 2026-07-29 14:04:21 -06:00
Ben Barclay
f5b68ad58b Merge origin/main into feat/hsp-sync-client
Resolves the PR's conflict with main (2252 commits). Two conflicts, both
"each side added an independent block in the same place" — kept both:

- gateway/run.py — the housekeeping loop. This branch adds the Skill Sync
  pulls inside the CURATOR_EVERY branch (12-space indent); main adds a
  stale-session auto-archive as a sibling `if` at loop level (8-space).
  Different scopes, so the naive union would have mis-nested the archive
  block into the curator branch; kept each at its own indent level.
- tools/skill_manager_tool.py — the _edit_skill result dict. This branch
  appends the org auto-propose note; main appends
  _add_description_prompt_preview(). Independent, order-insensitive.

No behaviour dropped from either side.

Verified: 3552 passed / 0 failed across 63 suites (scope regenerated to
include main's new maybe_auto_archive / _add_description_prompt_preview
consumers) via scripts/run_tests.sh. `hermes sync` and `hermes sync status`
still work against a live token, resolving the production plane default.

The Pyright Optional-parameter warnings in skill_manager_tool.py are
pre-existing on main (`content: str = None` etc.), not introduced here.
2026-07-29 13:01:36 -07:00
teknium1
27b1377b4c refactor(web): extract git/profiles/cron routes to APIRouter modules (web_deps seam; route-table equality verified) 2026-07-29 12:23:28 -07:00
Francesco Bonacci
847e401b74 feat(computer_use): align cua-driver 0.9 contracts
Salvaged from PR #67807 by @f-trycua onto current main.

- Foreground gate: discover delivery_mode support from the live tools/list
  inputSchema.properties (fail closed), not the never-shipped
  input.delivery_mode capability token
- bring_to_front: standalone strict-schema MCP tool (inject_session=False),
  separate approval scope, requires foreground
- Verdict precedence: confirmed > unverifiable (verify before retry) >
  suspected_noop/refusal (escalate); surfaced as explicit verdict field
- Typed cua_browser_* route inside computer_use (browser_route.py) with
  exact-binding, adapter-injected session, snapshot-scoped refs
- Per-Hermes-session backend isolation + release_computer_use_session seam
  wired into AIAgent.close()
- Recorded 0.9 tools/list fixture replaces fabricated capability tokens
2026-07-29 12:19:37 -07:00
teknium1
bcb352eeab refactor: registry-owned execute() on CommandDef — informational commands unified (thin slice) 2026-07-29 12:11:24 -07:00
teknium1
ba7da1332c refactor: single-owner model switch parsing + effective-model resolution (kills the api_server/run.py divergence class) 2026-07-29 11:54:09 -07:00
teknium1
bd93ccb890 refactor(gateway): shared fence-aware markdown chunker core (yuanbao-derived) + canonical table-row splitter 2026-07-29 11:53:59 -07:00
teknium1
5b751dc0ad chore: remove unused imports and dead locals (ruff F401/F841 sweep)
Cleans F401 unused imports and F841 dead local assignments across
root *.py, agent/, hermes_cli/, tools/, gateway/, cron/, tui_gateway/
(tests/, plugins/, skills/ excluded).

Intentionally KEPT (false positives / test-patch surfaces):
- agent/transports/__init__.py package re-exports
- cli.py browser_connect re-exports (DEFAULT_BROWSER_CDP_URL area,
  used by tests/cli/test_cli_browser_connect.py)
- hermes_cli/main.py _prompt_auth_credentials_choice /
  _model_flow_bedrock_api_key (accessed via main_mod attr in tests)
- gateway/run.py aliased replay_cleanup + whatsapp_identity re-exports
  and _PORT_BINDING_PLATFORM_VALUES (test-referenced)
- hermes_cli/web_server.py get_running_pid (tests monkeypatch it) and
  _OAUTH_TOKEN_URL availability probe
- hermes_cli/config.py get_process_hermes_home re-export (noqa'd F811
  chain) and yaml availability-probe import
- hermes_cli/nous_subscription.py managed_nous_tools_enabled
  (tests patch hermes_cli.nous_subscription.managed_nous_tools_enabled)
- try/except ImportError availability probes (env_loader, tts_tool,
  mcp_tool, web_server anthropic OAuth block)
- tools/web_tools.py noqa F401 re-exports
- hermes_cli/setup_whatsapp_cloud.py:263 'proceed' skipped: possible
  missing-guard bug, flagged for separate review
- unused function parameters (signature changes out of scope)

Side-effect RHS calls preserved where only the binding was dead
(e.g. web_server proc = _spawn_hermes_action -> bare call).
2026-07-29 11:53:39 -07:00
embwl0x
88c19a4edf fix(cli): repair Windows cua-driver autostart registration for paths with spaces
Detect a missing cua-driver-serve scheduled task after the installer runs
and retry registration via Start-Process -FilePath/-ArgumentList instead of
interpolating the binary path into a PowerShell command string (which splits
at the first space in a username-space path).

Salvaged from #60880 by @embwl0x. Related: #60808.
2026-07-29 11:35:33 -07:00
teknium1
c2eda92fd0 perf(config): stop deepcopying config on per-turn read-only paths
Four hot-path consumers paid a full config deepcopy per read:

- telemetry gate relay_shared_metrics.enabled() — runs 2-3x per agent
  turn (2x per API call from lifecycle hooks + 1x per tool call) and
  called read_raw_config(), which deepcopies the whole raw config every
  call. New read_raw_config_readonly() serves the cached dict directly:
  248 us -> 4.6 us per call (54x) on Teknium's real 77-key config.
- interruptible_streaming_api_call local-endpoint stale-timeout branch
  called load_config() once per API call for every local-model user.
- gateway get_inbound_media_max_bytes() + _get_ephemeral_system_ttl_default()
  called load_config() on per-message paths. All three switched to
  load_config_readonly() (345 us -> 12 us; PR #28866 lineage).

Together these account for ~90% of the ~1,900 deepcopy primitives per
turn measured in the 26-call stubbed-LLM profile.

read_raw_config_readonly() keeps the (mtime_ns, size) freshness key so
config edits are picked up next call, and preserves the identity
invariant (cache-miss returns the same object later hits serve) —
regression-tested with 'is', per the PR #28866 identity-bug lesson.
The mutable read_raw_config() is unchanged for save-path callers.

581 targeted tests green (config, relay metrics x2, ephemeral reply,
platform base, new readonly suite).
2026-07-29 11:33:41 -07:00
teknium1
595a408f40 rebase fix-up: carry perf(update) 3a69e34702 changes into relocated _cmd_update_check/_cmd_update_impl
The rebase onto main (which landed 3a69e34702 touching the two functions
this branch moves to update_cmd.py) resolved main.py to the moved-out
state; this commit re-applies the perf commit's function bodies at their
new home so no behavior from main is lost. Bodies extracted verbatim
from origin/main via AST.
2026-07-29 10:59:54 -07:00
teknium1
c64a4d75e5 refactor: extract dashboard process-hygiene helpers to dashboard_procs.py 2026-07-29 10:59:54 -07:00
teknium1
0e7c4018f7 refactor: hoist cmd_sessions out of main() into sessions_cmd.py 2026-07-29 10:59:54 -07:00
teknium1
927463efcc refactor: extract update pipeline to hermes_cli/update_cmd.py (mechanical move) 2026-07-29 10:59:54 -07:00
teknium1
2006cd5895 refactor(gateway): declarative busy_policy on CommandDef replaces hand-written mid-run command chain 2026-07-29 10:53:56 -07:00
teknium1
ed33ebca1d refactor: canonical config loaders for behavioral reads + guarded raw-read primitive (kills the managed-scope/env-expansion drift class)
The disease: ~15 scattered raw yaml.safe_load(config.yaml) reads that
silently miss managed-scope overlay, ${ENV_VAR} expansion, profile-aware
pathing, and root-model normalization. Every new config feature needed an
N-site sweep (incident chain 9cbcc0c9c8732293cf87b0e47a98f91928aa0443). This commit assigns every raw read to an owner and adds a
lint-guard test so the class cannot regrow.

New primitive (additive-only change to hermes_cli/config.py):
  read_user_config_raw(path=None) — reads the user file EXACTLY as
  written; docstring states it is ONLY legal for write-back round-trips
  and raw-file diagnostics. Behavioral reads must use
  load_config()/load_config_readonly().

BEHAVIOR FIXES (class-a sites migrated to a canonical loader — these
previously read values that could DIFFER from the effective config):

  gateway/run.py _try_resolve_fallback_provider → _load_gateway_runtime_config
    keys: fallback_providers/fallback_model (provider, model, base_url,
    api_key). Drift fixed: a managed-pinned fallback chain was ignored;
    an api_key of "${OPENROUTER_API_KEY}" reached the resolver unexpanded.
  gateway/run.py GatewayRunner._load_provider_routing → same loader
    key: provider_routing. Drift fixed: managed-pinned routing prefs and
    ${VAR} templates were ignored.
  gateway/run.py GatewayRunner._load_fallback_model → same loader
    keys: fallback chain. Same drift as above.
  gateway/run.py GatewayRunner._refresh_fallback_model
    keeps the raw primitive (its last-known-good-on-parse-failure contract
    forbids the fail-open loader, which returns {} on a torn write) but now
    applies managed overlay + env expansion inline. Drift fixed: chain
    edits under managed scope / env templates were previously frozen out.
  tui_gateway/server.py _load_cfg (72 behavioral call sites)
    now = raw read + managed overlay (pre-existing) + NEW ${VAR} expansion,
    split from a new _load_cfg_raw() write-back primitive. Drift fixed:
    e.g. custom_prompt: "hello ${VAR}", agent.system_prompt, model,
    api_key/base_url templates reached sessions unexpanded. DEFAULT_CONFIG
    is deliberately NOT merged (callers treat missing keys as unset;
    `_load_cfg() == {}` sentinels and _save_cfg round-trips depend on it).
  tui_gateway/server.py _profile_configured_cwd
    keys: terminal.cwd of a NON-launch profile. Drift fixed: managed
    overlay + ${VAR} expansion now apply (load_config() would resolve the
    wrong profile's home, so the raw primitive + inline pipeline is used).
  plugins/platforms/telegram/adapter.py _reload_dm_topics_from_config
    → load_config_readonly(). keys: platforms.telegram.extra.dm_topics.
    Drift fixed: managed overlay + profile-aware pathing + expansion.
  plugins/memory/holographic _load_plugin_config → load_config_readonly().
    keys: plugins.hermes-memory-store.*. Same drift class.

WRITE-BACK ROUND-TRIPS (class-b: stay raw BY DESIGN via read_user_config_raw;
merging defaults/overlay would pollute the saved user file):
  gateway/slash_commands.py: model persist x2, _save_gateway_config_key,
    memory/skills write_approval toggles
  gateway/platforms/yuanbao.py auto-sethome
  tui_gateway/server.py _write_config_key + all cfg→_save_cfg blocks
    (reasoning show/hide/full/clamp, details_mode[.section], prompt)
    → new _load_cfg_raw()
  plugins/memory/holographic save_config

RAW-FILE DIAGNOSTICS + presence-sensitive bridges (class-c: stay raw,
now via the shared primitive with an explanatory comment):
  hermes_cli/doctor.py x5 (model validation, stale-root-keys, .env drift,
    deprecation sweep, memory-provider probe — the latter two keep their
    inline managed overlay where they had one)
  gateway/run.py _bridge_max_turns_from_config and the module-level
    TERMINAL_*/HERMES_* env bridge (bridging merged defaults would export
    all of DEFAULT_CONFIG into the environment; both keep their inline
    overlay + expansion)
  hermes_cli/send_cmd.py env bridge (same presence-sensitivity)
  hermes_cli/gateway.py multiplex-conflict probe (reads the DEFAULT root's
    config, not the active profile's — load_config is the wrong owner)
  hermes_cli/profiles.py / hermes_cli/web_server.py / tools/wake_word.py
    multi-profile reads (load_config targets only the ACTIVE profile home)
  cron/jobs.py _resolve_default_model_snapshot and cron/scheduler.py
    run_job config read keep their existing inline overlay+expansion but
    now share the primitive (their fail-open + last-value semantics and
    the deliberate no-defaults merge are preserved exactly).

Failure-semantics audit: every migrated site preserves its exact previous
behavior on missing file ({} / early return) and parse failure (raise into
the caller's existing except, warn, last-known-good, or fail-open) —
read_user_config_raw intentionally mirrors bare open()+safe_load semantics
(raises on parse errors, {} only on FileNotFoundError/non-dict root).

Guard: tests/hermes_cli/test_config_read_guard.py scans the tree for
yaml.safe_load within 6 lines of a 'config.yaml' reference outside an
explicit ALLOWLIST (hermes_cli/config.py, gateway/config.py, gateway/run.py
fallback path, hermes_cli/managed_scope.py which reads the MANAGED file,
gateway/readiness.py parse-health probe) and fails on new offenders.

E2E: tests/hermes_cli/test_config_loader_e2e.py runs a subprocess with a
temp HERMES_HOME (config.yaml containing ${E2E_PROMPT_SUFFIX}) plus a
HERMES_MANAGED_DIR overlay pinning agent.reasoning_effort, asserting
tui _load_cfg resolves "hello world"/"high" while _load_cfg_raw +
_save_cfg round-trip the template and user value verbatim with no
managed/default leakage.
2026-07-29 10:53:29 -07:00
teknium1
d7a4065568 perf(local-endpoints): disk L2 for server-type + ollama ctx probes, faster timeouts
Local-model users paid a fresh probe waterfall on EVERY CLI cold start
inside AIAgent.__init__: detect_local_server_type (up to 4 HTTP GETs,
2s timeout each on a hung server) + /api/show (3s timeout). The
existing caches were in-process only, so back-to-back invocations
(chat -q, cron ticks, subagents) re-paid the network every time.

- New 300s-TTL disk L2 at HERMES_HOME/cache/local_endpoint_probes.json
  for detect_local_server_type verdicts and query_ollama_num_ctx
  results. Only SUCCESSFUL probes persist (a down server never pins a
  negative verdict); stale entries pruned on write; corrupted cache
  degrades to a miss; atomic writes. 300s is strictly fresher than the
  1h in-process TTL that already accepts server-swap staleness.
- models.dev fetch timeout 15 -> (5, 10) connect/read tuple: a
  blackholed connect stalled the first-turn critical path 15s; now
  fails in 5s (matches the OpenRouter fetch convention, #46620).
- _auto_detect_local_model timeout 5 -> (2, 3): runs inside
  _get_model_config() at startup against a LOCAL endpoint; a hung local
  server cost 5s before the banner.

E2E (real HTTP server, two fresh subprocesses, isolated HERMES_HOME):
proc1 = 2 HTTP hits, proc2 = 0 HTTP hits, identical results
(ollama/131072), probe wall 74.5 -> 35.5 ms. 222 targeted tests green
incl. 9 new disk-L2 contract tests.
2026-07-29 10:52:13 -07:00
brooklyn!
c8f911112c
Merge pull request #74033 from NousResearch/jb/session-detail-profile-stamp
fix(sessions): always stamp owning profile so cross-profile open works toward default
2026-07-29 12:39:09 -05:00
teknium1
fedd689d37 perf(config): one raw config.yaml parse per process instead of 3-4
Counted with an open()/read_text audit hook on a real 'hermes --version'
run: config.yaml was parsed 3x before load_config() even ran — once by
env_loader._load_secrets_config, once by main.py's early redact/ipv4
bridge (bespoke yaml.load), once by hermes_logging._read_logging_config.
Each raw parse is 1-3 ms with libyaml plus an open/stat — pure
duplication since all three want the same raw dict.

All three now route through read_raw_config()'s existing (mtime_ns,
size)-keyed shared cache:

- env_loader._load_secrets_config: uses the shared reader when reading
  the process HERMES_HOME (the cache key's home); other homes (profile
  seeding) keep the isolated direct parse. Parse-error isolation is
  preserved — the shared reader also swallows errors and returns {}.
- main.py early bridge: drops the bespoke yaml.load for read_raw_config
  (managed-scope overlay unchanged).
- hermes_logging._read_logging_config: prefers the shared reader,
  falls back to the direct fast_safe_load parse when hermes_cli.config
  isn't importable.

Measured: config.yaml opens per 'hermes --version' 3 -> 1.
348 targeted tests green (env_loader + secret sources + applied-homes +
bitwarden + hermes_logging + config).
2026-07-29 10:32:25 -07:00
brooklyn!
abd9edbea3
Merge pull request #74234 from NousResearch/bb/session-pins-server-owned
Pins are server-owned, so they survive paging and follow you between apps
2026-07-29 12:31:38 -05:00
teknium1
3a69e34702 perf(update): cut redundant network + subprocess work from hermes update
Four fixes on the updater path:

1. uv self update freshness gate + timeout (managed_uv.py): the network
   self-update ran on EVERY hermes update — including the 'Already up to
   date!' fast path — with NO timeout (unbounded hang risk offline). Now
   skipped when it succeeded within 7 days (stamp file under
   HERMES_HOME/cache), capped at 60s, force= override available. The
   CVE-driven vulnerable-runtime repair probe is NEVER gated — it still
   runs on every invocation.

2. Drop the second network fetch from the pull step (main.py): the update
   flow fetched origin/<branch>, counted commits, then ran
   'git pull --ff-only origin <branch>' — a SECOND fetch of the same ref
   (~0.5-1.5s). Now merges the already-fetched tracking ref via
   'git merge --ff-only origin/<branch>'; the diverged-history reset
   fallback is unchanged.

3. Probe the upstream remote locally before fetching it (_cmd_update_check):
   non-fork installs have no 'upstream' remote, and --check burned a
   failed network attempt (~0.3-1s) on every run before falling back to
   origin. 'git remote get-url upstream' (~1ms local) now gates the fetch.

4. Desktop rebuild check reads the content-hash stamp in-process before
   spawning 'hermes desktop --build-only' (a full CLI re-import, ~1-3s)
   just to learn nothing changed. Stamp errors fall through to the
   subprocess path unchanged.

Savings on a no-op 'hermes update': ~2-6s (uv self-update 0.5-3s +
second fetch 0.5-1.5s + desktop spawn 1-3s when applicable).
187 targeted updater tests green incl. 5 new stamp-gate tests.
2026-07-29 10:16:44 -07:00
teknium1
9179fb72ea refactor: extract web_server Pydantic models to web_models.py (pure schema move) 2026-07-29 10:15:07 -07:00
teknium1
326764e255 refactor: table-driven config migration registry (17 if-blocks → (version, fn) table, byte-identical semantics) 2026-07-29 10:14:32 -07:00
teknium1
3d48f893da refactor: single build_subprocess_env() factory for all child-process spawns (profile + secret-scrub single owner) 2026-07-29 10:14:11 -07:00
teknium1
7c198c5e44 refactor: single shared Retry-After parser 2026-07-29 10:13:50 -07:00
teknium1
58b78f5a0a perf(banner): scope startup update-check fetch to origin/main
The banner update-check ran an unscoped 'git fetch origin', transferring
all ~1,400 remote heads (measured 3.0s dry-run vs 0.55s scoped, and up to
70s on a cold ref store) and frequently burning its full 10s timeout on
slow links. cmd_update already scopes its fetch for exactly this reason.

A scoped 'git fetch origin main' updates both the origin/main tracking
ref (full-clone count path) and FETCH_HEAD (shallow compare path), so
behind-count semantics are unchanged — verified empirically on a full
clone (rewound tracking ref restored to tip, count correct) and a
--depth 1 shallow clone (FETCH_HEAD updated, boundary preserved).
2026-07-29 10:09:14 -07:00
Teknium
5081551f09 fix(voice): full-duplex agent-turn listener — interrupt by voice during generation AND playback
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.
2026-07-29 10:08:53 -07:00
Brooklyn Nicholson
3290807e60 feat(api): session lists carry the pin flag and never drop a pinned row
The three list endpoints (`/api/sessions`, `/api/profiles/sessions`, and
the batched sidebar route) now request the pinned back-fill and expose
`pinned` as a real JSON boolean alongside `archived`.

Both merge paths re-window rows after sorting, which would have thrown
away exactly what the back-fill fetched, so pinned rows survive the cap.
The sidebar's "load more" signal discounts them — they arrive past the
LIMIT by design and would otherwise fake a full page on a short list.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Co-authored-by: konsisumer <11262660+konsisumer@users.noreply.github.com>
Co-authored-by: eason2026 <209090628+eason2026@users.noreply.github.com>
2026-07-29 12:00:31 -05:00
teknium1
1fe06115d1 refactor: extract DEFAULT_CONFIG + OPTIONAL_ENV_VARS to config_defaults.py (pure data move) 2026-07-29 09:34:18 -07:00
teknium1
19055492aa fix: route stray HERMES_HOME hardcodes through get_hermes_home() (profile + native-Windows safety) 2026-07-29 09:33:48 -07:00
Teknium
aff380ddbe
Merge pull request #74176 from NousResearch/fix/cua-driver-refresh-pin-confirmed-version
fix: pin cua-driver refresh to the release check-update confirmed
2026-07-29 09:19:52 -07:00
Teknium
5fdc9d2a9d fix: pin cua-driver refresh to the release check-update confirmed
The upstream cua-driver installer scripts on trycua/cua@main carry a
baked default version that Release Please bumps in the release PR
*before* the release assets are published. During that window an
unpinned installer run 404s on the asset download and the
`hermes update` cua-driver refresh fails with:

    error: download failed: The remote server returned an error: (404) Not Found.
    ⚠ cua-driver refreshing did not complete. Re-run manually: ...

Observed live 2026-07-29: baked version 0.14.0 vs latest published
release 0.13.1 — every `hermes update` run with an out-of-date driver
hit the warning until upstream publishes the assets.

We already know the correct version: `cua-driver check-update --json`
returns `latest_version` straight from the GitHub Releases API, whose
entries by definition have published assets. When the check positively
confirms an update, export that version as CUA_DRIVER_RS_VERSION into
the installer child env (both install.sh and install.ps1 honour it over
their baked default), so the refresh downloads the release that
actually exists instead of racing the upstream release pipeline.

Malformed / missing latest_version values fall back to the previous
unpinned behaviour. The explicit `hermes computer-use install
--upgrade` force path and fresh installs are unchanged.
2026-07-29 09:02:19 -07:00
Teknium
7de33cc57e
Merge pull request #64536 from victor-kyriazakos/feat/gateway-health-diagnostics
feat(monitoring): gateway health & diagnostics OTLP export
2026-07-29 08:58:33 -07:00
Teknium
c19fd5c505 fix(cli): wire hermes import-agent parser into main argparse tree
The import-agent subcommand (24c3c27ba8) shipped with its parser builder
and handler logic but build_import_agent_parser() was never called from
main.py, making the documented and unit-tested command uninvocable.
Register it alongside the other subcommand builders.
2026-07-29 08:48:05 -07:00
Teknium
64beb25a35 fix(cli): stop hard-wrapping streamed paragraphs; prefer OSC 52 over SSH
Streamed responses no longer insert real newlines at terminal width —
logical lines are emitted whole and the terminal soft-wraps them, so
highlight-copy rejoins the full line (emulators only keep linebreaks
the app actually printed). This is the CLI equivalent of the TUI's
selection copy, which reads logical source lines from its screen
buffer. TTFT perception is preserved by mirroring the unfinished
line's tail into the spinner status text instead of chunk-printing.

/copy now prefers OSC 52 when running over SSH (SSH_CONNECTION /
SSH_TTY / SSH_CLIENT) — native tools there write the REMOTE clipboard,
which is never what the user wants. The CLI's OSC 52 writer also gains
tmux/screen DCS passthrough wrapping, mirroring the TUI's
wrapForMultiplexer. Fixes #31528 for the CLI surface.

Sabotage-verified: restoring the old chunk emitter fails 3 of the new
tests (hard-wrap detection, spinner mirror, unbreakable-run split).
2026-07-29 08:42:16 -07:00
victor-kyriazakos
1773752c8c Merge remote-tracking branch 'origin/main' into feat/gateway-health-diagnostics-monitoring
# Conflicts:
#	uv.lock
2026-07-29 15:37:14 +00:00
Teknium
c15f9b71a5 fix(voice): spoken barge-in works on every TTS playback path (CLI + gateway/desktop backends)
Premise check on live main: barge-in machinery EXISTS for the per-turn
STREAMING pipeline only — cli.py chat() arms _voice_barge_in_monitor and
tui_gateway _tts_stream_begin arms _tts_stream_barge_in_monitor. What was
actually broken for spoken interruptions:

1. CLI whole-file fallback (_voice_speak_response_async — used whenever
   streaming TTS cannot start: sounddevice missing, requirement probe
   fails): NO monitor was ever armed, so talking over the reply did
   nothing. Now arms _voice_barge_in_monitor in continuous voice mode.
2. Gateway fallback speak (tts_queue None → speak_text thread) and the
   voice.tts RPC (desktop-triggered speech): speak_text ran bare, and
   its internal streaming dispatch created a PRIVATE stop event nothing
   could reach — uninterruptible even by stop_playback(). New
   _speak_text_with_barge() runs the same barge monitor beside the speak
   thread; hermes_cli.voice.speak_text/_speak_text_streaming accept an
   external stop_event so a barge cuts the streaming pipeline too.
   Stop-phrase handling and voice.transcript submission are inherited
   from the shared monitor (merged #73933 behavior preserved).
3. False barge during TTS (the reason interruption "worked" then
   self-cancelled or fired randomly): salvaged PR #71083 by @beardedeagle
   (previous commit, kept authorship) — rolling-window VAD floor, 8x
   multiplier, 4000-RMS trigger ceiling, barge_in_grace_seconds (2s)
   before the mic opens, min-floor clamp. barge_in_grace_seconds is now
   documented in DEFAULT_CONFIG.

Desktop spoken barge (renderer mic via voice-barge-in.ts) already covers
both its live-stream and fallback speech paths — verified, no change.
2026-07-29 08:24:00 -07:00
Teknium
df093bf33c feat(voice): calm ambient "thinking" sound while the agent works in voice chat
Long thinking/tool stretches in a voice conversation are dead air — the
user cannot tell whether the agent is alive. New: quiet, repeating soft
bubble blips while the agent works and no speech audio is flowing.

- tools/voice_mode.py: numpy-synthesized blips (no binary assets) — two
  alternating low pitches (G4/E4) with pitch glide + smooth attack/decay
  envelopes, ~0.8-1.2s randomized spacing, volume = voice.beep_volume * 0.5.
  start_thinking_sound(should_play=...) / stop_thinking_sound() daemon-loop
  lifecycle; macOS-TCC-safe (sounddevice output gated there → silent skip,
  no per-second afplay churn). New mark_audio_output_active()/
  is_audio_output_active() ref-count wraps play_audio_file and the
  streaming OutputStream sentence writes so "audio is flowing" is accurate.
- Config: voice.thinking_sound (default true) off-switch.
- cli.py: starts when a voice-mode turn begins, per-blip gate skips while
  TTS speaks / mic records / barge capture owns the mic; stopped in the
  chat() finally on every exit path.
- tui_gateway/server.py: same lifecycle around _run_prompt_submit turns
  (voice mode on), gated on is_audio_output_active + continuous capture.
- Desktop: renderer owns voice-conversation audio, so a matching WebAudio
  implementation (src/lib/thinking-sound.ts, same envelope/pitches) runs
  while conversation status === "thinking"; honors voice.thinking_sound
  (via config store) and the shared sound-mute toggle; stops instantly on
  speaking/listening/end.
2026-07-29 08:24:00 -07:00
Teknium
9e5f1b619c fix(voice): silent cycles never end the chat while the agent is busy or TTS is playing
The continuous-voice no-speech counter (3 strikes -> voice off) counted
every silent capture cycle unconditionally. During a long agent turn
(thinking/tool-calling for minutes) or while TTS is speaking, the user
is CORRECTLY silent — those cycles ended the voice chat under them.

- hermes_cli/voice.py: new set_voice_busy_probe() seam + _voice_activity_held()
  (TTS-playing via the existing _tts_playing Event, agent-busy via the
  registered probe). Both the continuous-loop strike path and the
  force-transcribe single-shot strike path skip counting while held.
  Fail-open: a broken probe counts cycles as before.
- tui_gateway/server.py: registers _any_session_running() as the probe
  on voice.record start (voice is process-global; any running session holds).
- cli.py: classic CLI strike path skips counting while _agent_running
  or TTS playback is in flight.

Stop phrase and barge-in still work during the hold (own paths).
Includes a fixture fix for the #71083 cherry-pick: the fake tools.tts_tool
module needs _load_tts_config (main's tts_streaming imports it).
2026-07-29 08:24:00 -07:00
yoniebans
a8ec50f681 fix(sessions): always stamp owning profile so cross-profile open works toward default
Session rows served without ?profile= carried no profile field, so in
multi-profile desktops the default profile's sessions circulated unowned:
resolveStoredSession cached them profile-less, resolveSessionProfile returned
undefined, and session.resume targeted whichever gateway was active -- opening
a default-profile session from a non-default window failed with
"session can't be found" while the reverse direction worked (#67603 family).

Server: GET /api/sessions/{id} and GET /api/sessions now stamp profile/
is_default_profile unconditionally -- the serving profile is always known
(_cron_default_profile() when the request is unscoped).

Renderer: resolveStoredSession treats a profile-less $sessions cache hit as
unresolved when >1 profile exists (falls through to the stamped by-id ladder)
and back-fills the active profile on bare by-id hits from older backends, so
unowned rows are never re-cached.

Verified via CDP against a live 4-profile renderer: bare by-id GET returned
hasProfileField:false and the stale cache rows matched; with the fix both
lookups return the owning profile and the resume routes to the right backend.
2026-07-29 13:54:12 +02:00
kshitijk4poor
222ea2b6c9 refactor: fold simplify-code review findings
- extract _commit_registry/_note_refresh_failure shared by the background
  worker and foreground stage-4 (identical 4-step success + failure paths
  were duplicated); worker now commits under _models_dev_fetch_lock so a
  failing background refresh can never re-arm the backoff immediately
  after a successful force_refresh committed (unsynchronized-write race)
- add should_clear_context_pin_async to hermes_cli/route_identity.py
  (matching the get_model_context_length_async precedent) and use it at
  the 4 async gateway sites instead of inline asyncio.to_thread wraps;
  the sync _format_session_info site keeps the sync call (already
  off-loop via its callers' to_thread)
- test the background-refresh success path (the PR's primary new
  behavior): disk saved, mem cache swapped, backoff cleared, in_flight
  reset — mutation-checked
- replace the race-prone spin-wait on _models_dev_refresh_in_flight with
  a named-thread join in the backoff test
2026-07-29 17:13:48 +05:30
kshitijk4poor
ccf7129ed0 fix: restore zero-arg fetch_models_dev call on default paths
The branched call shape in get_provider_info/get_provider is deliberate:
~69 test sites across tests/hermes_cli and tests/gateway monkeypatch
fetch_models_dev (and get_provider_info) with zero/single-arg lambdas.
Passing allow_network= unconditionally broke 5 tests in CI slices 2/3/7.
Documented the constraint inline.
2026-07-29 17:13:48 +05:30
kshitijk4poor
11ca7eedf0 fix: follow-up hardening for salvaged #73621 + #35853
- _mark_stale_cache_grace only moves cache_time forward so a completed
  background refresh is never rewound to a 5-minute grace window
- clear _models_dev_refresh_in_flight if Thread.start() raises so a
  one-off thread-exhaustion failure doesn't disable refresh forever
- move empty-registry validation into _fetch_models_dev_from_network
  (was duplicated in the background worker and the foreground fetch)
- pass allow_network through as a plain kwarg in get_provider_info and
  hermes_cli.providers.get_provider instead of the branched call shape
- refresh the stale module docstring (no bundled snapshot exists; the
  resolution order now describes stale-serve + background refresh)
2026-07-29 17:13:48 +05:30
StellarisW
8c50aaceb6 fix(gateway): keep models.dev refreshes off event loop 2026-07-29 17:13:48 +05:30
kshitijk4poor
24a56f027c fix(lsp): expose lsp.idle_timeout in config, harden reaper, log reaps
Follow-ups on top of @DonutsDelivery's salvaged reaper commit (#64141):

- create_from_config() now parses lsp.idle_timeout (invalid values fall
  back to DEFAULT_IDLE_TIMEOUT) — previously the constructor knob was
  unreachable from config.yaml (config exposure adapted from #36892 by
  @0xbWy and #68091 by @9miya20)
- canonical default declared in hermes_cli DEFAULT_CONFIG so config
  discovery surfaces the knob (per sweeper review note on #64980)
- reaper loop survives transient sweep errors instead of dying and
  silently re-opening the leak (gap flagged in #68091 review)
- eventlog.log_reaped(): one INFO line per sweep + clears the
  log_active announce cache so respawns re-announce at INFO
- docs: replace the stale 'no idle-timeout reaper' paragraph with the
  new lifecycle description + config reference
- tests: reuse-refresh protection (the regression teknium's sweeper
  requested on #64141), reaper-survives-error, config propagation,
  invalid-value fallback, DEFAULT_CONFIG/manager-constant sync
2026-07-29 17:11:30 +05:30
kshitij kapoor
22492f0c46 refactor: extract atomic_write_text to utils.py; fix write-failure error handling
Deduplicates the mkstemp→fsync→atomic_replace pattern that existed in
three places: agent_import.py (added by #72983), MemoryStore._write_file,
and skill_manager_tool._atomic_write_text. All three now call a single
utils.atomic_write_text helper.

Also wraps the atomic_write_text call in _merge_memory_entries with
try/except OSError so a write failure records a per-item error instead
of propagating uncaught and aborting the entire import with no record.

Follow-up to #72983.
2026-07-29 16:49:07 +05:30