Commit Graph

294 Commits

Author SHA1 Message Date
kshitij 5bff3984be fix(tests): update two more append_message.call_args assertions to append_messages_batch
CI-caught: test_verification_stop_caching and test_tui_gateway_server::test_native_vision_turn_persists_a_renderable_image_ref both assert on append_message.call_args, but the flush loop now calls append_messages_batch. Same class of test-fake fallout fixed in 5 other files — these two were missed.
2026-08-03 20:43:38 +05:30
kshitij b58b3adb9b perf(tui-gateway): batch branch-seed history copies (whole-bug-class)
Sibling sites of the per-message flush pattern: both branch-seed
paths (session.branch in methods_session.py and the lazy seed persist
in server.py) copied the parent history row-by-row -- one transaction
per row, and a branch seed can be hundreds of rows. Route both through
SessionDB.append_messages_batch. The server.py path also gains real
atomicity: _branch_seed_persisted assumed every row landed, which the
per-row loop could not guarantee.
2026-08-03 20:43:38 +05:30
Daniel Blank cd41454dfc fix(gateway): let Desktop omit duplicate transcripts on session resume
Salvage of #69926: omit_messages support ported from the PR's
tui_gateway/server.py base onto the post-split methods_session.py
layout. When a Desktop client passes omit_messages=true on
session.resume / session.activate, the RPC returns messages: [] with
messages_omitted: true and an accurate message_count, skipping the
potentially multi-megabyte compression-lineage serialization over the
WebSocket; Desktop hydrates the transcript via the authenticated REST
route in parallel.

The PR's bundled cron-outputs endpoint and codex quiet-timeout bump
were dropped from this salvage as unrelated (invited back separately).
2026-08-03 13:45:08 +05:30
kshitij cc04825c5d fix: content-based diff for model-switch marker merge (#76870)
The original PR #77274 used positional slicing (current_history[len(history):])
to detect the model-switch-only mutation.  But _append_model_switch_marker
strips prior markers in-place before appending the new one, so when a prior
marker existed (every switch after the first in a session), the net length
delta is zero and the slice produces an empty list — the merge path is dead
code for the common case.

Replace with a content-based diff: strip markers from both the turn-start
snapshot and the current history, then check that the non-marker content is
identical.  This correctly handles the strip-and-replace behavior.

Also guard against auto-compression making result["messages"] shorter than
the turn-start history — use the full result as the base when that happens.

Added test covering both no-prior-marker and prior-marker cases.
2026-08-03 10:48:49 +05:30
joaomarcos a93969dbd1 fix(tui): snapshot history after pending model switch applies (#76870)
Deferred model switches append a marker and bump history_version at
turn start; the dispatcher was snapshotting history before that
mutation, so the version-mismatch guard rejected the turn's own
result as a stale/concurrent write. Move the snapshot to after
_apply_pending_model_switch/_sync_agent_model_with_config, under
history_lock, so the turn's own preparatory mutation is included in
its baseline while the anti-stale guard still catches real external
writes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:56:24 +05:30
Teknium 719c1b0366 test(tui_gateway): route lookup-failure regression through has_live_for_session 2026-08-02 15:11:37 -07:00
null-runner 6101c92dd1 test(desktop): follow upstream orphan teardown path 2026-08-02 15:11:37 -07:00
Nicholas Mariani 719a3da11f test(desktop): cover finalizing orphan delegation state 2026-08-02 15:11:37 -07:00
null-runner 820f63e842 [verified] fix(desktop): preserve background delegates across session switches 2026-08-02 15:11:37 -07:00
xrazai e57a8f5cb9 fix(gateway): preserve delegates during session reaping 2026-08-02 14:02:08 -07:00
kshitij 3fad8fdc45 polish(mem): readonly config read, debug-level trim logs, loud frame test
Simplify-pass follow-up on the #66355 salvage:

1. _config_settings runs on EVERY trim attempt (before the cooldown
   check) and only reads — swap load_config for load_config_readonly.
   Deep-copying the whole config per attempt generates exactly the
   allocator garbage this module exists to release. Tests re-seamed.

2. Trim-failure logs demoted warning->debug at all 3 periodic sites
   (gateway housekeeping, idle reaper, slash worker): sibling failure
   branches in the same loops log at debug, and a persistent failure
   (e.g. broken import after a partial update) would otherwise warn
   every 60s forever.

3. The frame-inspection test now asserts the expected locals exist
   before reading them — a rename in _run_prompt_submit fails the test
   loudly instead of vacuously passing on None.
2026-08-02 22:44:38 +05:30
Ryder Freeman da43a8527b feat(mem): config-driven allocator trim with telemetry and lifecycle coverage
Add config-driven glibc malloc_trim for long-lived Hermes processes:
- hermes_cli/mem_trim.py: trim_memory() with configurable cooldown,
  RSS snapshot telemetry, and forced-trim INFO logging
- gateway/run.py: periodic trim in gateway housekeeping loop
- tui_gateway/server.py: trim in idle reaper (~every 5 min)
- tui_gateway/slash_worker.py: trim on turn boundary
- run_agent.py: force trim on agent close
- hermes_cli/config.py: context.memory_trim config section
  (enabled, cooldown_seconds, log_every_n, info_log_min_delta_mb)

CSA tier-4 reviewed (4 rounds, 0 HIGH/MEDIUM/CRITICAL remaining).

Supersedes PR #63708 + #64591 with enhanced telemetry and gateway/slash_worker coverage.
2026-08-02 22:44:38 +05:30
Frowtek 6dec4bfc88 fix(tui_gateway): install the parent profile's secret scope on session.branch
alongside every set_hermes_home_override() call site on the tui_gateway
path, and converted session.create, session.resume (both the _make_agent
and _init_session scopes), the lazy _build resume and the per-turn submit
handler. session.branch was missed.

The branch handler already binds the parent's HERMES_HOME and its
profile-scoped state.db — its own comment says it mirrors
session.create/resume — but builds the branched agent with no secret scope.
get_secret() then falls through to process os.environ, which in an
app-global/remote backend is the LAUNCH profile's environment: a session
branched off profile X authenticates with profile Y's credentials. That is
the same cross-profile resolution #67605 fixed for the sibling paths, and it
is silent (multiplexing is only activated by the messaging gateway, so the
fail-closed UnscopedSecretError path never fires here).

Install set_secret_scope(build_profile_secret_scope(parent_home)) for the
build and release it in the same finally block as the home override.

Salvage-note: code relocated from tui_gateway/server.py to tui_gateway/methods_session.py (session handlers moved after the PR was opened); test patch targets unchanged — HandlerRegistry rebinds handlers onto server.py globals
2026-08-02 10:01:16 -07:00
dsad f86693c2f9 fix(tui): fail closed when prompt.submit cannot persist history truncation
Desktop edit / regenerate / restore-checkpoint send truncate_before_user_ordinal
on prompt.submit. The handler rewrote session['history'] first, then called
replace_messages, and on failure only printed to stderr and still started the
turn. When the durable write fails, in-memory history is already truncated
and history_version is bumped while state.db still holds the pre-edit tail.
The agent flush is append-only for history-dict identities, so the new
exchange is appended on top of the 'undone' turns — durable zombie history.

Fix: persist first; only then mutate memory. On failure return 5008 and
leave memory/DB unchanged.

Based on #72876 by @necoweb3. Adapted: the handler moved from
tui_gateway/server.py to tui_gateway/methods_prompt.py since the PR's base.
2026-08-02 12:18:40 +05:30
fyzanshaik d50858584c fix(tui): preserve queued image ownership 2026-08-01 15:58:42 +05:30
Brooklyn Nicholson 609cd28b17 feat(tui): rank the slash menu by the skills you actually use
The `/` menu was a flat first-30 slice of the completer's output, and the
completer emits every registry command before the first skill. On a
230-skill install that meant a bare `/` filled all 30 rows with commands
and offered no skill at all, while `/p` cut off inside the alphabetical
skill block — dropping /proving-a-fix-works (471 invocations) and
/pr-update (160) but keeping /pretext (2).

Spend the limit per kind and rank the skill block by recorded usage
(the same .usage.json count Capabilities shows), most-used first and A-Z
within a tie. A bare `/` is browsing, so bundled skills that shipped with
Hermes and were never opened are dropped as noise; a typed query is a
search, and a search that hides a match is broken, so there nothing is
pruned and the ranking only reorders. An argument stage keeps the order
its own command chose.
2026-08-01 01:17:41 -05:00
Teknium c0689c3bcb test(tui): make _load_enabled_toolsets assertions tolerant of first-release back-filled toolsets
The two exact-list assertions in test_tui_gateway_server froze the toolset
list and broke the moment _RECENTLY_SHIPPED_TOOLSETS back-filled bfl onto a
saved platform list — the exact behavior the sibling change ships on purpose.
Assert the invariant instead: the expected base set is present, and anything
extra must be inside _RECENTLY_SHIPPED_TOOLSETS (vacuously exact again once
that set empties between releases).
2026-07-30 16:34:08 -07:00
brooklyn! d8a9c17dae
fix(tui): keep the pending model shown at turn end instead of blipping back (#74766)
A model picked mid-turn is applied at the next turn start, but the end-of-turn
session.info (_emit_settled_session_info) reads the still-live agent — the OLD
model — and clobbered the optimistic paint, so the UI showed the new model, then
snapped back to the old one when the turn ended, then switched for real on the
next turn. Report the queued pick's model/provider in _session_info while a
switch is pending (it IS the model the next turn runs), so the display stays on
the user's choice through the settle. Cleared once the switch applies.
2026-07-30 10:32:55 +00:00
brooklyn! f27d45e288
feat(tui): reach the model picker without wrecking your draft, and switch mid-turn (#74756)
* feat(tui): Ctrl+O opens the model picker without clearing your draft

Reaching the model picker meant typing /model, which forces you to wipe
whatever you'd already drafted. Bind Ctrl+O to open the same picker overlay
directly, leaving the composer untouched. Ctrl+O is added to the textInput
pass-through allowlist so the composer doesn't swallow it, mirroring the
existing Ctrl+X session-switcher path.

* feat(tui): apply a mid-turn model switch at the next turn instead of rejecting it

Picking a model while a turn was streaming hit a 4009 'session busy' reject:
switch_model() mutates the agent's model/provider/base_url/client in place and
the worker thread reads those every iteration. Now config.set queues the pick
in session[pending_model_switch] and _apply_pending_model_switch applies it on
the turn thread at the next turn start, before any model call — no race, no
interrupt, no waiting on the client rebuild. The TUI paints the pick optimistically
and notes '(applies next turn)'.
2026-07-30 10:18:30 +00:00
Brooklyn Nicholson 5ecc35f9bb fix(tests): teach gateway-server fakes the include_row_ids kwarg
Slice 1 fell over on eight DB fakes with frozen get_messages_as_conversation
signatures — the new opt-in kwarg is part of the method's contract now, so
the fakes accept **_kwargs like the real SessionDB. Also opts the child-watch
resume projection into row ids: it feeds the same _history_to_messages as the
desktop resume, so reactions on a watched child session address rows the same
way.
2026-07-30 00:08:28 -05:00
AIalliAI 74d9110a7a test(tui): scope close_race orphan-cleanup assert to own session key
test_session_create_close_race_does_not_orphan_worker asserted the
process-global len(unregistered_keys) >= 1: a leaked _build thread from
another session.create test in the same shard can append a foreign key
and falsely satisfy the assert. Scope both the wait loop and the assert
to this test's own stored_session_id, matching the own-key scoping the
no-race companion test already uses for the same flake class.

Salvaged from PR #46189 by @AIalliAI (rebased onto the pruned suite).
2026-07-29 18:55:10 -07:00
Gille 94d1dff50d fix(wake): route desktop control and select input devices 2026-07-29 14:04:21 -06:00
teknium1 c5d37bb95c test(tui): pin approval-mode fixtures to env HERMES_HOME for canonical resolver
_load_approval_mode now delegates to tools.approval._get_approval_mode,
which reads config via hermes_cli.config.load_config — that path resolves
HERMES_HOME from the environment, not the server module's _hermes_home
attribute. The four approval-mode tests only monkeypatched the module
attribute, so the resolver silently read the developer/CI real config
(default 'smart') instead of the temp fixture. Behavior assertions are
unchanged (invalid → manual fail-safe, YAML off-bool → 'off', three-way
persist + live emit); the fixtures now also set HERMES_HOME so the
canonical read path sees the same temp config.
2026-07-29 11:19:24 -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
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 6fdfdc1597 feat(voice): "Say <stop-phrase> to end the voice chat" notice on voice-mode start (CLI/TUI/desktop, i18n)
One owner for the wording: voice_stop_hint() in tools/voice_mode.py —
sources the phrase from voice.stop_phrases (first entry) so a custom
phrase renders correctly, and returns "" when the feature is disabled
(stop_phrases: []) so no surface shows a hint.

- CLI: printed in /voice on output (style-matched dim notice).
- TUI: voice.toggle action=on now carries stop_hint; the Ink client
  renders it in the "Voice mode enabled" block (older gateways omit
  the field — no hint, no crash).
- Desktop: the renderer voice loop never touches tools/voice_mode.py,
  so the phrase is read from config (voice.stop_phrases → $voiceStopPhrase
  store, seeded in use-hermes-config) and shown as an info toast when a
  voice conversation starts. i18n: en/ja/zh/zh-hant/ar.
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
beardedeagle be424703c4 fix(voice): rolling-window VAD, duplicate render suppression, TUI gateway mirror
Replace one-shot VAD calibration with a rolling deque window that
continuously recalibrates the noise floor throughout TTS playback,
preventing false barge-in triggers from stale calibration. Add a
grace period before VAD activates so TTS playback establishes first.
Suppress duplicate text rendering when token streaming is enabled.
Mirror the barge-in and TTS stream stop logic to the TUI gateway path
so both CLI and gateway use the same VAD semantics.

Rolling-window VAD:
- 90th percentile of rolling window (~3s) for noise floor
- 8x multiplier (was 5x) for TTS volume variation headroom
- 4000 RMS trigger ceiling so genuine speech can still trip
- min_floor clamped to SILENCE_RMS_THRESHOLD * 2
- sustained_ms=1000, calibration_ms=800

Barge-in grace period (barge_in_grace_seconds, default 2.0s):
Delays VAD activation so TTS playback establishes before the mic opens.

Duplicate render suppression:
When streaming_enabled, pass display_callback=None to
stream_tts_to_speaker so the token stream is the sole display path.

TUI gateway mirror:
Mirror _tts_stream_stop and _tts_stream_barge_in_monitor changes to
tui_gateway/server.py so both code paths use the same VAD parameters,
grace period, and TTS CUT diagnostic logging.

Profile-scoped session DB and MoA progress events in tui_gateway/server.py
were necessitated by the TTS pipeline changes affecting session state
and event routing.

Normal-exit flag and TTS CUT diagnostic logging at all cut paths.

Regression tests:
- test_quiet_then_loud_playback_does_not_trip
- test_8x_multiplier_absorbs_tts_volume_spikes
- test_trigger_ceiling_lets_genuine_speech_trip
- test_silence_calibration_does_not_false_trip_on_tts
- test_tts_stream_stop_latches_interruption_for_next_turn
- test_tts_stream_stop_after_natural_finish_does_not_latch
- Profile-scoped session DB tests (10 tests)
2026-07-29 08:24:00 -07:00
Teknium ba13132298 fix(voice): bare stop phrase ends the voice chat on every surface, spoken or typed
Saying OR typing a configured stop phrase (voice.stop_phrases, default
"stop") now ends the voice chat everywhere, not just classic CLI PTT:

- hermes_cli/voice.py: new explicit on_stop_phrase callback through
  start_continuous/stop_continuous. The force-transcribe path previously
  DISCARDED the stop phrase silently — with auto_restart=False the client
  re-arms the next capture, so the conversation never ended. Both halt
  paths now fire on_stop_phrase (fallback: on_silent_limit for legacy
  callers) as user intent, distinct from the no-speech timeout.
- tui_gateway/server.py: voice.record wires on_stop_phrase and emits
  voice.transcript {stop_phrase: true} after flipping HERMES_VOICE(_TTS)
  off and stopping streaming TTS — same teardown as /voice off. The TTS
  barge-in monitor stop-checks its transcript too. prompt.submit consumes
  a TYPED bare stop phrase at the server-side choke point when voice mode
  is on (returns {voice_stopped: true}, no turn starts).
- ui-tui: voice.transcript {stop_phrase} ends voice mode with a clear
  'voice chat ended' notice (distinct from the no-speech-limit message);
  submitPrompt releases the busy latch on a consumed voice_stopped reply.
- cli.py: _typed_voice_stop in process_loop — typing a bare stop phrase
  while voice mode/continuous is active ends voice mode instead of
  sending 'stop' to the agent; typed 'stop' outside voice mode is
  unchanged. Voice transcripts skip the check (already stop-checked).
- desktop: interceptsTypedVoiceStop — the composer's onSubmit ends the
  live voice conversation (same path as clicking end on the pill) when a
  bare stop command is typed with no attachments; renderer-owned loop, so
  handled client-side like the existing spoken isVoiceStopCommand.
- tools/voice_mode.py: transcribe_recording never lets the Whisper
  hallucination filter swallow a configured stop phrase (e.g. 'bye'
  configured as a stop phrase is both a hallucination-blocklist entry and
  a stop phrase — stop-phrase check now wins).

Tests: continuous-loop signal (sabotage-verified), force-transcribe stop
signal + legacy fallback, hallucination-filter ordering, typed-stop CLI
unit tests (voice on/off/longer text), prompt.submit typed-stop gateway
tests, TUI vitest for stop_phrase event handling, desktop vitest for the
typed-stop interceptor.
2026-07-29 00:01:06 -07:00
Teknium 7e7f7d3059
Merge pull request #70509 from NousResearch/hermes/hermes-29661bf6
feat(voice): on-device wake words with open-vocabulary phrases and multi-profile voice routing
2026-07-28 17:58:33 -07:00
Brooklyn Nicholson 75bf13e032 feat(gateway): report per-skill usage and origin in commands.catalog
The catalog advertises every skill command but nothing about which ones
the user actually reaches for, so consumers can only sort them
alphabetically. Add a `skills` map keyed by slash command carrying the
activity count and origin (hub / bundled / local) already tracked in the
skills sidecars, read once per catalog build.

Additive: older clients ignore the field, and an unreadable sidecar
degrades to zero usage rather than failing the catalog.
2026-07-28 19:14:46 -05:00
Teknium 0cf58de85e
Merge remote-tracking branch 'origin/main' into wake-toggle-config
# Conflicts:
#	tests/test_tui_gateway_server.py
#	tui_gateway/server.py
2026-07-28 12:37:35 -07:00
Solitud1nem af7205bea5 fix(voice): thread max_recording_seconds through the TUI path, add behavior coverage
Review follow-up: the sweeper is right that the first commit only cured
half the dead config — the TUI gateway builds its recorder params
explicitly, so the cap never reached recordings started from the TUI.

- start_continuous() grows a max_recording_seconds param (default 0.0 =
  disabled, so existing callers keep today's behaviour) and applies it to
  the shared recorder next to the silence params
- tui_gateway voice.record start forwards the validated cap; corruption
  semantics now mirror the silence params everywhere: non-numeric/bool
  falls back to the documented 120 default (a hand-edited
  `max_recording_seconds: true` must not become a 1-second cap), while
  an explicit numeric <= 0 disables the cap
- cli.py wiring updated to the same corruption semantics

New coverage, per review:
- TestMaxRecordingCap drives the mocked InputStream callback past the
  cap during continuous loud speech (the silence branch physically can't
  fire there) and asserts the one-shot callback fires exactly once; plus
  the disabled-cap negative
- TestMaxRecordingSecondsConfigReal pins the CLI config assignment for
  the valid / zero / bool / garbage cases via _voice_start_recording
- test_voice_record_start_forwards_max_recording_seconds pins the TUI
  forwarding for the same matrix
2026-07-28 11:57:37 -07:00
Teknium 754fdbdc98
test: set HERMES_HOME in _persist_model_switch tests after save_config_value fix
The prior commit made save_config_value resolve its target via
get_hermes_home() (live env) instead of the import-time cli._hermes_home
constant. Two _persist_model_switch tests patched only cli._hermes_home
and wrote/read tmp_path/config.yaml, so the write now landed in the real
HERMES_HOME and the readback saw stale values. Set HERMES_HOME=tmp_path
(kept the _hermes_home patch for belt-and-suspenders). No production
change.
2026-07-28 10:52:28 -07:00
Brooklyn Nicholson e35c2f6049 fix(sessions): claim the cap slot on first turn, not on open
An open chat window took a session-cap slot at session.create/resume time.
Every desktop tile paint and every background reconnect-resume opens one, so
on a websocket-flappy host they accumulated: five parked desktop tabs filled a
5-slot cap and locked the messaging gateway (which shares the cap) out for
fourteen minutes while running no agents at all.

A slot held that way is invisible everywhere. An unprompted draft has no DB row
and the sidebar filters it out with min_messages=1, so the only way to diagnose
it was reading runtime/active_sessions.json by hand.

Claim on the first turn instead, mirroring the lazy contract
_ensure_session_db_row already uses for the row itself. Capacity now means an
agent can run rather than that a window exists, and anything holding a slot is
something the user can see.

Also reclaim leases whose session skipped teardown. _prune_dead only fires when
the owning pid dies, and a dashboard/serve backend runs for days, so a leaked
lease was held until restart. The owning process reconciles against the leases
it still holds, which is exact and needs no heartbeat write on the turn path.
2026-07-28 12:11:52 -05:00
teknium1 e8f9d471c6
feat(wake): the toggle IS the config — explicit on/off persists wake_word.enabled
Clicking the desktop ear button or running /wake on|off now writes
wake_word.enabled to config.yaml (live, saved for future sessions), so the
feature no longer requires hand-editing config before the UI toggle works.

- wake.start accepts persist:true (explicit gesture): flips
  wake_word.enabled on in config before arming; response reports
  enabled_persisted. Passive auto-arm paths (desktop gateway-ready,
  TUI reconnect) never pass it, so a mic can't become persistently
  enabled without a deliberate user action.
- wake.stop accepts persist:true: writes wake_word.enabled: false so
  auto-arm stays off next session; reports disabled_persisted.
- Split the refusal reason: 'disabled' (feature off in config — a
  persisted gesture turns it on) vs 'disabled_for_surface' (explicit
  wake_word.surface scoping, which persist does NOT override).
- Classic CLI /wake on|off and bare-toggle persist the flag too
  (skips the write when config already matches).
- Desktop tooltip now maps refusal codes to friendly text (mirrors
  the TUI's START_REASON_TEXT) instead of showing raw codes like
  disabled_for_surface.
- Docs: quick-start notes the toggle persists; ear-button mention.
2026-07-28 07:59:34 -07:00
Hermes Agent 2a35c8f0b8
feat(voice): route wake phrases to their profile — "hey <profile>" wakes that profile
One sherpa listener now enrolls every wake-enabled profile's phrase
(defaulting to "hey <profile name>") and reports WHICH phrase fired.
wake.detected gains a profile field; the desktop live-switches to the
matching profile (same path as the profile rail), opens a fresh session
there, and starts hands-free voice. The single-profile CLI/TUI print the
hermes -p switch command for foreign-profile phrases instead of
answering as the wrong profile. Opt out per listener with
wake_word.profile_routing: false.
2026-07-28 07:59:34 -07:00
Omid Saadat 5839aad13d
fix(wake-word): enforce single-owner lifecycle 2026-07-28 07:58:01 -07:00
kshitijk4poor cf258b6ae7 fix(session): widen display_kind filter to prompt.submit ordinal + rollback.restore
Phase 2 review found two sibling sites with the same bug class:
- truncate_before_user_ordinal in prompt.submit counted display_kind
  timeline rows as user turns, shifting the truncation target
- rollback.restore used the old pop-loop pattern that would pop a
  display_kind marker instead of the last real exchange

Both now use the same predicate (role==user and not display_kind)
matching list_recent_user_messages, /undo, /retry, and CLI resume.

Added tests for both paths.
2026-07-28 19:37:14 +05:30
Brooklyn Nicholson b3ba5570c9 Merge origin/main into bb/skill-chip-only
Keep skill-invocation projection helpers alongside the auto-continue
legacy display typing from #73250.
2026-07-28 04:06:32 -05:00
Brooklyn Nicholson 7ab5567953 fix(gateway): read an untyped recovery note as a timeline event
Pass the display type into the turn so the row is born typed, and
recognize the note's fixed prefix in _history_to_messages so the
untyped rows already on disk stop painting as user bubbles.
2026-07-28 03:54:21 -05:00
Brooklyn Nicholson dd39ec2694 fix(gateway): project a /skill turn onto its invocation for every client
A slash-skill invocation is persisted expanded — activation note plus the
entire skill body. _history_to_messages is the single display projection
every surface reads, so that payload rendered as a chat bubble anywhere a
session was resumed.

Project it here onto the invocation the user typed, and tag skill/bundle
dispatches with the same string so the live send matches. Rewind and
regenerate replay from what the transcript shows, so re-expand the
invocation server-side before running the turn: the replayed prompt is
identical to the original and no client ever holds the body.
2026-07-28 03:44:17 -05:00
Brooklyn Nicholson c883367bd2 fix(agent): keep interrupt-checkpoint scaffolding out of the steered transcript
A mid-stream steer persists an interrupted-turn checkpoint so the model knows
its reply was cut off. That scaffolding — "[This response was interrupted by a
user correction.]" and the "Visible response before the interruption:" header —
was written straight into message content, so every reload painted the raw
machinery as an assistant bubble (and merged it into the preceding tool-call
bubble). Steered transcripts became unreadable.

Reuse the existing display/replay split instead of inventing new surface:
- Carry the scaffolded form in the server-only api_content sidecar (the exact
  bytes replayed to the provider), keep content the user's/agent's real words.
- When nothing reached the screen there is no clean form, so mark the row
  display_kind=hidden — replayed to the model, dropped by every transcript
  surface, exactly like compaction-reference rows.
- Honor display_kind=hidden in the gateway's _history_to_messages projection
  (it only sniffed the [System: convention), so the checkpoint can't leak
  through the live/resume path to the TUI/CLI either.

The model still receives the full interrupted context on the wire; the
transcript shows the partial reply and the user's correction.
2026-07-28 01:05:02 -05:00
Teknium f9cd577915 feat(approvals): add cross-surface mode command
Co-authored-by: luxiaolu4827 <227715866+luxiaolu4827@users.noreply.github.com>
2026-07-26 21:13:03 -07:00
brooklyn! 6cea77303b
Merge pull request #72339 from NousResearch/bb/redirect-user-row
Preserve the original prompt when a mid-turn redirect corrects a turn
2026-07-26 20:09:46 -05:00
teknium1 62e8842d07 fix(tui): emit multi_select hint only when true — single-select clarify payloads keep the pre-existing protocol shape 2026-07-26 17:46:55 -07:00
Brooklyn Nicholson 2dd4cbbe61 fix(tui_gateway): keep the original prompt when a redirect corrects a turn
An accepted mid-turn redirect wrote its correction over inflight_turn["user"].
That field is the only user text session.resume can replay, so the prompt that
started the turn was gone the moment the user typed again while it ran. On the
next resume the client rebuilt the thread without it.

Record corrections in their own list instead, alongside the prompt. Renamed
_replace_inflight_user to _record_inflight_correction now that it appends.
_start_inflight_turn rebuilds the dict wholesale, so corrections cannot leak
into a later turn.
2026-07-26 19:43:23 -05:00
teknium1 f34a69b1cd test(tui): prove agent build installs the selected profile's secret scope
Sabotage-verified: fails when the set_secret_scope call in
_start_agent_build is removed.
2026-07-26 16:19:52 -07:00
brooklyn! 79b4a40568
Merge pull request #72220 from NousResearch/bb/tool-row-dedupe
fix(tools): stop the inline tool row stuttering its verb and repeating the command
2026-07-26 16:25:50 -05:00
teknium1 af8d698b41 fix(mcp): propagate profile HERMES_HOME override into shared discovery owner; restore stdio startup + WSTransport tests
Follow-up to @LionGateOS's #72135 salvage:
- Route ensure_mcp_discovery_started through hermes_cli.mcp_startup's
  shared owner instead of a hand-rolled bare thread, keeping the start
  lock, retry-after-zero-connected allowance, and interactive-OAuth
  suppression. The shared owner now captures the caller's context-local
  HERMES_HOME override and re-installs it inside the discovery thread,
  so discovery reads the selected profile's mcp_servers (#67605).
- Restore stdio TUI startup discovery in main() and the
  _mcp_discovery_enabled retry gate in wait_for_mcp_discovery, both
  dropped by the original branch.
- Restore the 3 WSTransport regression tests (send serialization,
  cross-batch ordering, drained-token ordering) deleted by the PR.
- Harden the profile-scoped discovery test against sibling-state leaks.
2026-07-26 14:14:03 -07:00