Commit graph

18911 commits

Author SHA1 Message Date
Brooklyn Nicholson
1e355116a1 fix(desktop): keep composer chrome out of the pop-out gesture
The pill strips live inside the composer root, so their box sits within
the drag region and their gaps read as grab area. Exclude anything
marked composer-no-drag, matching how buttons and menu items are already
excluded.
2026-07-28 19:20:07 -05:00
Brooklyn Nicholson
9b01c74f00 feat(desktop): render the micro-action and underside strips
Pills pin to the top of the composer's overlay lane, outside the status
card and outside its scroller, so nothing stacks above them and a long
todo list can't scroll them away. The underside slot sits below the
surface. Both share one grid constant and one parent, so their left
edges match by construction rather than by matching numbers in two
files.

The strips take pointer events while their empty space falls through to
the pop-out drag region, which keeps the composer draggable by the band
its badges live in.
2026-07-28 19:20:00 -05:00
Brooklyn Nicholson
1cefad5523 feat(desktop): composer.microActions + composer.underside contribution areas
Two new seams on the composer, both through the existing contribution
registry: a data area whose providers resolve badge descriptors per
session, and a render area for a chrome-free strip below the surface.
Core registers nothing in either — they stay empty until something
contributes.

Providers resolve from live session context rather than registering
static entries, so a contribution can be conditional without the
reactive when() the registry deliberately doesn't offer.
2026-07-28 19:19:52 -05:00
brooklyn!
6c2a3b4bb2
Merge pull request #73673 from NousResearch/bb/desktop-eventbus
Desktop: event-driven live sync — gateway change broadcasts replace the always-on polls
2026-07-28 17:39:40 -05:00
Brooklyn Nicholson
fa8fb82d0e test(gateway): change-watcher behavior contracts; align status-snapshot test with 60s cadence
Real temp-HERMES_HOME tests for _broadcast_watched_changes: silent seed,
cron/sessions signature moves, the 2s sessions floor's trailing edge, the
pet signature staying 'off' without a renderable pet, meta payload on
pet.changed, and a broken probe never killing the pass. Part of #73618.
2026-07-28 17:21:58 -05:00
Brooklyn Nicholson
c14ae3796e feat(desktop): fs-watch the plugin dir + demote the status snapshot
- watchDirectory IPC (same registry/channel as the preview file watchers)
  replaces the disk-plugin door's 5s readdir poll; older shells without
  the capability keep the poll, which self-upgrades to the watch once the
  plugins dir exists.
- Status snapshot: 15s → 60s, skips round-trips while hidden, and
  refreshes immediately on visibilitychange so re-focus never shows stale
  health.

Part of #73618.
2026-07-28 17:21:58 -05:00
Brooklyn Nicholson
fe64cafa33 feat(desktop): event-driven live sync — change broadcasts replace the always-on polls
One controller (store/live-sync.ts, the workspace-events twin for gateway
state): gateway-event.ts routes pet.changed / cron.changed /
sessions.changed into tick atoms, gated on the active profile like
skin.changed; gateway.ready's change_events flag arms them; a gateway wipe
resets the capability.

Consumers subscribe to ticks and demote their polls to slow backstops
(legacy cadence is kept verbatim against older backends):

- session.active_list 1.5s → sessions.changed + 30s backstop
- cron list 30s → cron.changed + 5min backstop
- messaging lists 10s → trailing sessions.changed refresh + legacy poll
  only when events are unavailable
- open messaging transcript 5s → sessions.changed + 30s backstop
- pet.info 3s/15s → pet.changed, NO timer on event-capable backends
  (users with no pet used to poll hardest); enabled=false broadcasts
  clear the mascot with zero round-trips
- cron runs page/peek 8s → cron.changed + 60s backstop

Idle renderer traffic drops from ~89 req/min to the backstops (~5/min).
Part of #73618.
2026-07-28 17:21:58 -05:00
Brooklyn Nicholson
3378e528e7 feat(gateway): generalize the skin watcher into a change watcher (pet/cron/sessions broadcasts)
The gateway had exactly one global broadcast (skin.changed) fed by a 0.5s
signature loop. Generalize that loop into a table of cheap on-disk
signatures so the process broadcasts what it already knows:

- pet.changed     — active pet slug/sheet-revision/scale moved (/pet, hatch)
- cron.changed    — cron/jobs.json mtime moved (CRUD + scheduler tick)
- sessions.changed — state.db/-wal mtime moved: the cross-process signal
  for messaging-gateway and cron-run writes (#58671), floored to one
  broadcast per 2s so a streaming turn's append burst coalesces

gateway.ready now carries change_events:true so clients can demote their
legacy polls to slow backstops while staying compatible with older
backends. Groundwork for #73618.
2026-07-28 17:21:57 -05:00
brooklyn!
43ddefd08f
Merge pull request #73675 from NousResearch/bb/edit-models-hover
fix(desktop): match Edit Models row hover to the model picker
2026-07-28 17:19:44 -05:00
brooklyn!
f9d7bca252
fix(desktop): overlay scrollbars on conversation code blocks (#73670)
The app-wide `.scrollbar-dt *` webkit rules force classic always-on
gutters on every descendant scroller. Opt code-card scroll surfaces out
via `.scrollbar-overlay` so Electron keeps platform overlay bars (fade
in on scroll, no reserved track).
2026-07-28 22:13:54 +00:00
Brooklyn Nicholson
6d83a79f93 fix(desktop): match Edit Models row hover to the model picker 2026-07-28 17:13:20 -05:00
brooklyn!
ed5fd3503d
fix(desktop): clear surface height vars from the attached surface, not a detached node (#73638)
When the status stack collapses (last subagent/background/queue item
finishes), React removes the stack div before the layout-effect cleanup
runs. Resolving the surface via closest('[data-chat-surface]') from that
now-detached node falls back to the document root, so the stale measured
height was cleared from the wrong element and stayed on the surface —
inflating --thread-last-message-clearance (the thread's bottom padding)
until the next publish. Same latent hazard in the composer's unmount
cleanup.

Capture the surface root while the node is still attached and clear from
it directly.
2026-07-28 17:07:22 -05:00
Atakan
20de37d409 fix: reject ambiguous MCP tool name collisions 2026-07-28 15:02:52 -07:00
hermes-seaeye[bot]
66e20786d0
fmt(js): npm run fix on merge (#73666)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 21:55:38 +00:00
Teknium
03af12c5c9 style(desktop): blank line between node: and vitest import groups
perfectionist/sort-imports requires spacing between builtin and
external import groups.
2026-07-28 14:47:36 -07:00
Teknium
20e0b26c4e test(desktop): run venv-blocker-scan tests under vitest
CI's electron vitest project collects electron/**/*.test.ts; the
node:test-based suite reported 'No test suite found'. Import
describe/it from vitest like sibling electron tests.
2026-07-28 14:47:36 -07:00
Teknium
1dd1c449bb fix(desktop): run venv-blocker scan async off the main-process event loop
The preflight's execFileSync froze the Electron UI event loop for up to
15s while psutil scanned the full process table. Convert
scanVenvBlockers to async execFile and await it in applyUpdates; tests
updated to async DI stubs. Also adds trailing newlines.
2026-07-28 14:47:36 -07:00
atakan g
d210500b54 fix(desktop): surface external venv update blockers 2026-07-28 14:47:36 -07:00
Atakan
01cb38e8ec fix(gemini): preserve bridged tool response name 2026-07-28 14:37:56 -07:00
Atakan
9dcb44a219 fix(schema): preserve dependentRequired property names 2026-07-28 14:37:19 -07:00
Siddharth Balyan
3cedac00b7
fix(credits): reintroduce grant_spent, gated on an in-session crossing (#73634)
* Revert "fix(credits): remove the 'Grant spent · $X top-up left' notice"

This reverts commit 5dc6a14c14.

* fix(credits): reintroduce grant_spent behind an in-session crossing gate

The removed notice nagged because its condition is a steady state for
accounts living on top-up, the latch is per-session, and the cold-start seed
runs the policy at every session open — every session re-announced
'Grant spent · $X top-up left'. Reintroduce it gated so only a session that
WATCHES the grant run out announces:

- seen_grant_unspent crossing gate, mirroring seen_below_90: opens when the
  session observes the grant meaningfully unspent (>= GRANT_UNSPENT_MIN_MICROS,
  1 cent — portal-seeded states derive micros from float dollars and can carry
  sub-cent residue where headers report exactly spent). Seeds never prime it.
- The gate guards only the show branch and is consumed by the announcement —
  one announcement per crossing. Header flicker (used_fraction None and back)
  clears the sticky line but cannot re-announce; a renewal re-opens the gate.
- new_credits_latch() centralizes the latch shape (agent build, lazy re-init,
  and the policy test helper all build through it).
- Tests: steady-state-open stays silent (seed + policy seams), live crossing
  announces once, flicker/renewal/residual cases locked; the rendering test
  primes the gate and asserts its leg count so a gate regression cannot
  silently shrink coverage.
2026-07-28 21:36:23 +00:00
Teknium
4f001742fd test(mcp): make discovery-lock tests cross-platform
CI (Linux) failed with ModuleNotFoundError: portalocker — it's a
win32-only dependency (concurrent-log-handler chain). Tests now lock
handles via a platform helper (fcntl on POSIX, portalocker on Windows),
mirroring production _try_acquire_mcp_discovery_lock.
2026-07-28 14:31:24 -07:00
Teknium
8151670712 fix(mcp): widen discovery lock wait to cover real discovery durations
The bounded lock wait was 10 x 0.2s = 2s, but the concurrent-discovery
scenario this lock exists for (#62771: hermes serve + gateway spawning
every MCP server twice) reports 40-60s discovery rounds. A 2s budget
guarantees the loser times out and runs unguarded exactly when the guard
matters. Widen to 240 x 0.5s = 120s; fail-soft unguarded fallback and
test overrides unchanged.
2026-07-28 14:31:24 -07:00
Atakan
362fc21577 fix(mcp): guard discovery with cross-process lock 2026-07-28 14:31:24 -07:00
Atakan
56bda4529b fix(computer_use): revive ended cua-driver sessions once 2026-07-28 14:24:30 -07:00
Atakan
8c5e846536 fix(gateway): bind HTTP auth to routed profiles 2026-07-28 14:22:18 -07:00
atakan g
3d30232eba fix(delegate): isolate async batches from parent interrupts
Detached background delegation batches (_batch_runner) no longer honor
the foreground parent's interrupt flag — a busy-submit interrupt in the
TUI/desktop previously fabricated 'interrupted' results for background
children that should outlive the turn. Explicit cancellation still works
via _batch_interrupt.

Rebased onto current main from PR #65040; both interrupt-suppression
regression tests aligned with the current _session test helper.

Original work by @AtakanGs in #65040.
2026-07-28 14:20:10 -07:00
atakan g
5423e26a7d fix(gateway): preserve rate-limit failure metadata 2026-07-28 14:19:14 -07:00
atakan g
713982a8f8 test(model-metadata): cover local Ollama fallback 2026-07-28 14:18:18 -07:00
atakan g
0c2d9aee0b fix(model-metadata): prefer local Ollama num_ctx 2026-07-28 14:18:18 -07:00
atakan g
d98ea211f9 test: cover both multiplex URL policy orderings 2026-07-28 14:17:45 -07:00
atakan g
7b18de5f40 fix: scope private URL policy per profile 2026-07-28 14:17:45 -07:00
atakan g
fa9217542d fix(desktop): preserve local model provider endpoint 2026-07-28 14:17:27 -07:00
Teknium
237b0f5d5e test: opt TestSpeakTextGuards out of the new audio-playback guard
The autouse _audio_playback_guard from this PR stubs voice.speak_text
globally — but these tests exercise speak_text itself with their own
playback stubs (no real audio possible). Mark real_audio_playback so
the guard yields; the tests' own monkeypatches keep speakers silent.
2026-07-28 14:07:21 -07:00
Teknium
bb09e3eaac test(tts): adapt salvaged #57681 tempfile-fallback test to registry streamers
Main's streaming path now resolves providers via
tools.tts_streaming.resolve_streaming_provider rather than constructing an
ElevenLabs client inline; stub the registry resolver instead so the test is
provider-agnostic and hermetic.
2026-07-28 14:07:21 -07:00
Teknium
f71d2d854f test(tts): cover microsecond default-output timestamp (salvaged #43911) 2026-07-28 14:07:21 -07:00
Teknium
42091a83b9 chore: map william.reed@acquia.com -> wreed4 for salvaged #68090 2026-07-28 14:07:21 -07:00
William Reed
946ed96785 fix(voice): reconcile NeuTTS backbone/codec GPU device strings
llama_cpp's GGUF backbone loader only enables n_gpu_layers for the
literal device string "gpu"; torch's codec only accepts "cuda". A
single --device value passed straight through can't satisfy both,
so config.yaml's documented tts.neutts.device: cuda silently ran the
backbone on CPU while the codec ran on GPU.

Map cuda -> gpu for the backbone only; leave the codec on the
torch-native string. Measured 24.9s -> 13.1s per synthesis call on
an RTX 5070 Ti (neutts-air-q4-gguf) with this fix.
2026-07-28 14:07:21 -07:00
Que0x
555d4e10ad fix(tts): close temp WAV handle before playback in streaming fallback
`_play_via_tempfile` passes the NamedTemporaryFile *object* to `wave.open()`.
`wave.open()` flushes but does not close a file it did not open itself (by
name), so the OS handle to the temp WAV stays open. On Windows that open write
handle blocks the system player from reading the file and blocks the
`os.unlink()` cleanup (WinError 32, silently swallowed by `except OSError:
pass`), leaving orphaned temp .wav files piling up. The fallback runs whenever
no sounddevice output stream is available (sounddevice not installed / no audio
device), which is common on headless and Windows setups.

Close the temp file handle before invoking the player and again in the finally
block (idempotent), so the player can read the file and `os.unlink()` can
remove it.

Regression test drives `stream_tts_to_speaker` with the sounddevice path forced
unavailable and asserts the temp handle is closed before `play_audio_file` is
called (mutation-verified: reverting the close leaves the handle open at play
time and the test fails). Cross-platform.
2026-07-28 14:07:21 -07:00
3ASiC
45f7030786 fix(tts): bind MiniMax credentials to their region endpoint 2026-07-28 14:07:21 -07:00
Chaz Dinkle
7b0a575c82 fix(tts): use microsecond timestamp to prevent concurrent output path collision
Concurrent TTS requests within the same second (e.g. from voice prefetch)
generated identical tts_YYYYMMDD_HHMMSS.ogg paths, causing a race where
one request would unlink the file while another was still writing — resulting
in 'produced no output' errors.

Adding %f (microseconds) to the strftime format makes collisions practically
impossible. Registered as PATCH-006.
2026-07-28 14:07:21 -07:00
Teknium
4c7c51fcb2 refactor(gateway): one media-cache cleanup loop — extend pruning to video + screenshot caches
Follow-up to salvaged PR #56473: dedupe the five cleanup_*_cache bodies
into a shared _cleanup_cache_dir() helper, add cleanup_video_cache() and
cleanup_screenshot_cache() (with get_screenshot_cache_dir()), and drive
all five from a single (name, fn) loop in _start_gateway_housekeeping()
instead of one copy-pasted try/except per cache. Covers the video/
screenshot half of #56427.
2026-07-28 14:07:21 -07:00
vb3
a61edf952f fix(gateway): add cleanup_audio_cache() and wire it into gateway housekeeping
Adds cleanup_audio_cache() to gateway/platforms/base.py and wires it into
_start_gateway_housekeeping() in gateway/run.py, matching the existing
image/document cache cleanup pattern.

Re-implements the fix proposed in #16473, which targeted the now-deprecated
_start_cron_ticker() and has since stalled with a merge conflict against
main. _start_gateway_housekeeping()'s docstring already claimed audio
cleanup ("prunes the image/audio/document cache"); this makes the code
match it.

Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
2026-07-28 14:07:21 -07:00
Solitud1nem
562c8b418f test(voice): drive the micro-pause test from the explicit clock too
Review follow-up: test_micro_pause_tolerance_during_speech was left on real
sleeps, so it kept the wall-clock dependency the rest of this PR removes.
Its three sleeps (50 ms, 50 ms, 60 ms) feed the same monotonic gate at
tools/voice_mode.py:561 that the other two tests were converted for.

It was measured rather than assumed before being left out: 0 failures in 40
runs, because on Windows sleep() rounds up to the timer tick, so sleep(0.05)
really costs ~62.5 ms and the three sleeps clear the 150 ms gate by ~37.5 ms
-- wider than the 15.625 ms GetTickCount64() quantum that made the other two
flake. That margin is an accident of sleep granularity rather than anything
the test guarantees, so it is not worth keeping the bet.

Driving it from fake_clock makes the timeline exact (0.05 + 0.05 + 0.06 =
0.16 against the 0.15 gate, dip 0.05 under the 0.1 tolerance) and drops the
real sleeping from the suite. Mutation-checked: widening the gate at :561
fails the test.
2026-07-28 14:07:21 -07:00
Solitud1nem
309ed7ec65 test(voice): drive silence detection tests with an explicit clock
test_silence_callback_fires_after_speech_then_silence and
test_custom_threshold_and_duration space their audio frames with
time.sleep(0.06) and check the result against 50 ms thresholds. That
leaves a 10 ms margin, which only survives if the clock is finer-grained
than the margin — and on Windows it isn't. time.monotonic() there is
GetTickCount64() with 15.625 ms resolution until CPython 3.13 moved it to
QueryPerformanceCounter(), so a sleep that really lasts 62.5 ms measures
as 46.9 ms often enough to matter, landing under the threshold. Depending
on which sleep got clipped, either the speech-confirm gate misses or the
silence timer never matures — which is why the same flake shows up on two
different asserts.

Both tests now advance a hand-driven clock instead of sleeping, so the
arithmetic is exact on every platform and neither test waits on real time.
The fixture patches the `time` name inside tools.voice_mode rather than
time.monotonic itself: voice_mode.time IS the stdlib module, so patching
through it would swap the clock out from under every other importer for
the duration of the test.

Measured on Windows 11 / Python 3.11.9, 20 runs of each test: 6/20 and
3/20 failed before, 0/30 after. The same machine on Python 3.13, where
monotonic() is QueryPerformanceCounter(), never failed either test — that
comparison is what pinned the clock resolution as the cause rather than
the detection logic. Linux CI never sees this: clock_gettime is
nanosecond-resolution there.
2026-07-28 14:07:21 -07:00
Zavian
98fea1323b test(gateway): keep Discord document-handling tests hermetic against host DNS
These tests mock the actual download; stub is_safe_url so host DNS/proxy
mappings for cdn.discordapp.com can't decide whether document handling is
exercised.

Residual hunk salvaged from PR #25426 — the rest of that PR's hermeticity
fixes (bedrock botocore fakes, faster_whisper module fakes, kittentts
plain-sequence PCM, web-provider SSRF stubs) have since landed on main in
equivalent form.
2026-07-28 14:07:21 -07:00
obelisk-complex
63be8ce863 fix(tests): stop the test suite speaking aloud and launching a browser
Running the suite could take over the machine it ran on. Two real
effects, both now closed:

- **The suite spoke through the speakers.** Once any test drove the
  `voice.toggle` RPC with `action="tts"`, the handler set
  `HERMES_VOICE_TTS=1` in the *live process environment*, and the flag
  outlived that test. Every later test that drove a turn to completion
  then fed its final response text to `hermes_cli.voice.speak_text` on a
  background thread - real synthesis, real playback, no API key needed
  (the default `edge` provider is keyless). A developer heard the fixture
  string "partial answer complete" out loud. Because the flag is set from
  inside the process, `scripts/run_tests.sh`'s `env -i` never protected
  against this.

  `tests/conftest.py` now blanks `HERMES_VOICE`/`HERMES_VOICE_TTS` per
  test, and a new autouse `_audio_playback_guard` stubs `speak_text` and
  its playback binding outright, so the speakers stay shut even inside the
  test that sets the flag itself. `@pytest.mark.real_audio_playback` opts
  out.

- **The suite launched Chrome.** `tests/tools/test_browser_supervisor.py`
  spawned a real browser on any machine with Chrome on `PATH`. Its
  docstring promised a `HERMES_E2E_BROWSER=1` gate that existed nowhere in
  the code. That gate is now real, and the file is marked `integration`
  so the default marker filter excludes it. `scripts/run_tests.sh`
  forwards `HERMES_E2E_BROWSER` so the documented manual run still works.

Miscellanea

- `tests/test_audio_playback_guard.py`: regression cover for both defences,
  driving the real `voice.toggle` handler rather than a stand-in.
2026-07-28 14:07:21 -07:00
Teknium
c911a5f10f chore: contributor email mappings for voice-platform-inbound salvage 2026-07-28 14:06:56 -07:00
轩辕
368d3488d2 fix(weixin): preserve voice transcript origin 2026-07-28 14:06:56 -07:00
Kewe63
cf6ff8cd30 test(gateway/weixin): add integration handoff regression for #27300 voice routing
Address teknium1 review on PR #47125: the existing adapter-level tests cover
the prerequisites (_download_voice / _collect_media / _extract_text) but not
the gateway-runner handoff. Add TestWeixinVoiceGatewayHandoff covering the
final routing contract:

- An inbound Weixin voice item carrying Tencent Cloud text is surfaced as a
  VOICE MessageEvent whose media is audio/silk (the shape the runner keys off
  to enter Hermes' STT pipeline), exercised through the real _process_message.
- That VOICE event's body does NOT leak Tencent's STT text, so the central
  transcript replaces it rather than being trusted.
- A VOICE/audio/silk event reaches the real GatewayRunner
  _enrich_message_with_transcription (patched as a spy), proving the runner
  handoff the adapter-only tests missed.
2026-07-28 14:06:56 -07:00