Commit graph

9462 commits

Author SHA1 Message Date
Hermes Agent
0ae305ed4e
feat(voice): open-vocabulary wake phrases via sherpa-onnx KWS
New "sherpa" wake_word provider: the configured phrase is BPE-tokenized
at runtime against a small streaming zipformer KWS model (~13 MB English,
one-time download cached under HERMES_HOME), so ANY typed phrase works
with zero training — including per-profile phrases like "hey coder".

wake.sherpa lazy-dep group + [wake] extra grow sherpa-onnx/sentencepiece;
requirements probe routes per provider; sensitivity maps onto sherpa
keywords_threshold. E2E-verified on real audio (target phrase fires,
foreign phrase stays silent, reset drops buffered state).
2026-07-28 07:58:16 -07:00
Brooklyn Nicholson
9f84bc30bd
feat(voice): bundle the trained "hey hermes" model as the out-of-the-box default
From #53378: ships hey_hermes.onnx/.tflite (openWakeWord pipeline,
Apache-2.0) under tools/wakewords/, resolves the default (and hey_hermes
aliases) to the bundled file, ensures openWakeWord base feature models
are fetched for custom paths too, and updates config defaults + docs
from hey_jarvis to hey hermes.
2026-07-28 07:58:16 -07:00
Omid Saadat
5839aad13d
fix(wake-word): enforce single-owner lifecycle 2026-07-28 07:58:01 -07:00
Brooklyn Nicholson
edb12bf423
fix(voice): stop wake re-fire loop and empty-transcript error spam
Two bugs surfaced by the desktop wake conversation:

1. Runaway loop: wake -> voice -> resume -> wake fired again within
   ~200ms. openWakeWord keeps its rolling feature buffer across
   pause/resume, so on resume it immediately re-scored the "hey jarvis"
   captured before the pause and re-fired, reopening a session and
   restarting voice in a tight cycle. Reset the engine buffer on every
   detector (re)start so resume begins from clean audio.

2. Empty-transcript toast: a silent re-listen returns
   success:false / "… STT returned empty transcript", which the desktop
   transcribe endpoint turned into a 400 -> thrown error -> "Voice
   transcription failed" notification on every silent gap. Treat an empty
   transcript as no-speech: return {ok, transcript: ""} so the voice loop
   quietly re-listens. Real failures still 4xx/5xx.
2026-07-28 07:58:01 -07:00
Brooklyn Nicholson
86d5b8b90f
feat(voice): extend "Hey Hermes" wake word to TUI + desktop GUI
Makes the wake word a tri-surface feature with one configurable owner.

- wake_word.surface ("auto" | "cli" | "tui" | "gui") + shared
  wake_surface_enabled() gate consulted by every surface, so exactly one
  place owns the listener and the new session it opens.
- tui_gateway: wake.start/stop/pause/resume/status RPCs + a wake.detected
  event, sharing one server-side detector for both TUI and desktop. The
  detector yields the mic to voice.record (pause on capture start, resume
  on terminal) and to the desktop's browser mic (wake.pause/resume).
- TUI (Ink): arm wake.start on gateway.ready; on wake.detected open a
  fresh session and start voice capture.
- Desktop (Electron): arm wake.start on connect; on wake.detected open a
  fresh session.
- CLI now gates on wake_surface_enabled("cli"); /wake status shows surface.
- Tests for the surface gate; docs cover the surface knob + cross-surface.
2026-07-28 07:56:45 -07:00
Brooklyn Nicholson
5f43452e91
feat(voice): add "Hey Hermes" wake word to start a hands-free session
Adds an opt-in, on-device hotword listener for the CLI. With
wake_word.enabled (or /wake on), Hermes listens in the background for a
wake phrase; on detection it starts a fresh session, captures one
utterance through the existing voice pipeline, and answers — the
"Hey Siri" pattern.

- tools/wake_word.py: provider-pluggable detector (openWakeWord, free
  local default; Porcupine, premium) over the shared 16 kHz sounddevice
  capture path. Background daemon thread with pause/resume so it yields
  the mic during a voice turn.
- CLI wiring: startup listener (off-thread), on-wake flow, an idle
  watchdog that resumes the detector after each turn, cleanup hook, and
  a /wake [on|off|status] command.
- config.yaml wake_word section; PORCUPINE_ACCESS_KEY as an optional
  secret. Engines lazy-install via the [wake] extra.
- Hands a transcript to the input queue exactly like voice mode, so no
  system-prompt/cache mutation. No new core model tool.
- Tests (mocked, no live audio/network) + feature docs.
2026-07-28 07:56:45 -07:00
kshitijk4poor
2e9559adf0 test(compression): resolve context_length inside mock in overflow-warning fixture
CI shard 4 caught 4 failures the local baseline diff missed (local env
pollution made them look pre-existing): _make_compressor() constructs
under a get_model_context_length mock but the lazy deferral (#32221)
pushed resolution past the with block, so the 96K window / 75% floor the
tests rely on never materialized. Resolve inside the mock.
2026-07-28 20:04:58 +05:30
kshitijk4poor
49f50c68a0 fix(compression): coherent context_length setter — no-op guard, re-floor on new window, un-strandable init log
Review-pass findings on the lazy-init deferral:

- No-op guard: the codex app-server usage callback assigns
  compressor.context_length on EVERY response (same window each time).
  The setter unconditionally invalidated the derived budgets, wiping
  runtime corrections applied directly to threshold_tokens /
  tail_token_budget (aux-context threshold sync) — those persisted on
  main's eager init. Same-value assignment is now a no-op.

- Re-floor on genuinely new window: the setter invalidates budgets but
  previously kept the stale threshold_percent, so a codex window switch
  recomputed threshold_tokens from the new window with the old model's
  floored percent. Re-apply the raise-only small-context floor from
  _base_threshold_percent so percent and tokens derive from the same
  window (guarded with getattr for object.__new__ test instances).

- Init log extracted to _emit_init_summary_once() and also fired from
  the setter path, so a consumer assigning context_length before any
  read no longer strands the startup line forever.

- threshold_tokens getter resolves the window into a local before
  reading threshold_percent — correctness no longer depends on
  left-to-right argument evaluation order.

- reasoning_timeouts: fix inaccurate 'tuples are immutable' comment
  (the container is a list; safety comes from build-once-at-import),
  document why the slug stays in the tuple.

Adds TestContextLengthSetterCoherence (3 tests): same-value assignment
preserves overrides; new-window assignment re-floors both directions.
2026-07-28 20:04:58 +05:30
kshitijk4poor
e762a5a473 fix(compression): copy-on-write in image-shrink recovery so degraded images never reach stored history
With the selective prompt-cache copy (#57046), un-marked messages on the
decorated api_messages list share their nested content parts with the
persistent conversation history — the per-message copy in
conversation_loop is shallow and decoration now deep-copies only the
marked messages. try_shrink_image_parts_in_messages previously wrote the
re-encoded image INTO the aliased part/source dicts, so an
image-too-large retry on an Anthropic route would silently replace the
original image bytes in agent.messages (and persist the degraded copy).

Replace the in-place writes with copy-on-write: rebuild the content list
with fresh part/source/image_url dicts and reassign msg['content'] — a
top-level write on the per-call copy that never reaches history.

Adds two regression tests simulating the aliasing; both fail against the
old in-place implementation (mutation-verified).
2026-07-28 20:04:58 +05:30
kshitijk4poor
5ce8e34767 test(compression): resolve context_length inside mocks across suites hit by lazy-init deferral
Baseline diff vs clean upstream/main (182 pre-existing environmental
failures on both sides) showed exactly 6 PR-caused failures, all the same
mechanism: tests construct ContextCompressor under a
get_model_context_length mock and read threshold_percent/threshold_tokens
after the with block, or build via object.__new__ and assign
context_length AFTER the derived budgets (the setter now resets the
lazily-cached budgets).

- test_compression_small_ctx_threshold_floor: resolve inside _make()
- test_cjk_token_estimation: resolve inside mock
- test_per_model_compression_threshold: resolve inside mock (2 tests)
- test_pre_compress_memory_context: assign context_length before
  threshold/tail/summary budgets; add summary_target_ratio
2026-07-28 20:04:58 +05:30
kshitijk4poor
fd53fa3eac test(compression): resolve context_length inside mocks for tests added on main since PR base
TestThresholdTokensCap and TestLazyContextResolution landed on main after
the #38991 lazy-init base; they construct ContextCompressor under a
get_model_context_length patch and read threshold_tokens after the with
block. With deferred resolution the probe now fires lazily, so resolve
inside the mock (same pattern as the rest of the suite) and give the
lazy-resolution mock a real return_value.
2026-07-28 20:04:58 +05:30
kshitijk4poor
4da1abf789 test(caching): guard prompt-cache shallow-copy mutation-safety + byte-equivalence
Self-review found the deepcopy->shallow-copy change in
apply_anthropic_cache_control (#57046) had no test pinning the
"prompt caching is sacred / never mutate the caller's list" invariant.
The old test_returns_deep_copy only exercised the single marked message,
never an un-marked shared reference, so a regression that deep-copied too
little would pass the whole suite.

Add two tests: (1) caller list + every element left byte-identical after
the call, un-marked middle messages returned as shared references, marked
messages fresh copies, and mutating a returned marked message does not leak
upstream; (2) structural byte-equivalence vs a reference full-deepcopy
implementation across both native_anthropic modes and two TTLs.

Mutation-verified: neutering the per-message deepcopy makes test (1) fail.
2026-07-28 20:04:58 +05:30
kshitijk4poor
44bd0521a4 fix(compression): keep ContextCompressor init non-blocking when quiet_mode=False
The lazy-init change in #32221 deferred get_model_context_length() out of
ContextCompressor.__init__, but the "Context compressor initialized" log
(emitted only when quiet_mode=False) reads self.context_length,
self.threshold_tokens and self.tail_token_budget. Those property reads
resolve the deferred value, so the synchronous model-metadata probe still
ran inside __init__ on the interactive-CLI path — the exact blocking the
PR removed, just narrowed to the non-quiet path.

Emit the informative line once, on first context-length resolution, so
construction stays non-blocking on every path. Add a regression test
asserting no probe fires in __init__ with quiet_mode=False and that the
init log is emitted exactly once on first access.
2026-07-28 20:04:58 +05:30
Rod Boev
f8d6f79c1a test(agent): fix lazy-init rebase test fallout (#32221)
(cherry picked from commit 920013f8d9)
2026-07-28 20:04:58 +05:30
Rod Boev
958a81a1e4 perf(agent): defer synchronous httpx.post out of AIAgent.__init__ (#32221)
(cherry picked from commit 1fe63238b0)
2026-07-28 20:04:58 +05:30
dsad
ad6df5eb95 test(compression): recurse into control-flow stmts in AST walker
The structural AST walker in test_compression_session_id_persistence.py
only recursed into control-flow children found via iter_child_nodes(stmt).
When a session_entry.session_id assignment lives inside an `else` block
whose statements are all assigns (no nested If/Try/etc to trigger
_walk_node), the assignment was invisible to the walker and the test
florped with 'No assignments found'.

Walk the stmt itself when it is a control-flow node so its
body/orelse/finalbody (and Try handlers) are always expanded, regardless
of whether iter_child_nodes yields an inner control-flow child.
2026-07-28 19:38:11 +05:30
dsad
f6abc6a046 fix(gateway): write hygiene compressed transcript before rebinding session
Manual /compress already persists the rotated child transcript first and
only then repoints the live session_entry; a False rewrite_transcript
return keeps the entry on the original session_id so the conversation
stays reachable. Session hygiene auto-compress did the opposite: it
rebound session_id (and lease/topic) first, then called rewrite_transcript
without checking the return value. On a failed write the live entry
already pointed at an empty child SID and the turn continued — permanent
silent conversation loss. Persist first; rebind only after success.
2026-07-28 19:38:11 +05:30
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
dsad
748c12b148 fix(session): skip display_kind timeline rows in undo/retry turn targets
list_recent_user_messages and the in-memory /retry + session.undo walkers
treated every role=user row as a real user turn. Timeline bookkeeping
(model_switch, async_delegation_complete, auto_continue, hidden) is stored
that way, so /undo soft-deleted from a marker and /retry re-sent opaque
bookkeeping text. Exclude display_kind the same way CLI resume counting and
the prompt.submit ordinal path do.
2026-07-28 19:37:14 +05:30
liuhao1024
9e2f07e704 perf(dashboard): use GROUP BY for session stats instead of fetching 10k rows
Replaces the O(N) list_sessions_rich histogram in /api/sessions/stats
with a single GROUP BY query, reducing response time from ~575ms to
<1ms on large databases.

Original PR #48921 by @liuhao1024. Salvage fixes based on review
feedback from teknium1 and @wernerhp:

1. Preserve try/except guard — a DB error still degrades to empty
   by_source instead of failing the whole stats response.
2. GROUP BY COALESCE(source, 'cli') — the original GROUP BY source
   could emit duplicate 'cli' keys (NULL group + literal 'cli' group)
   that the dict comprehension silently dropped.
3. Add exclude_children=True — list_sessions_rich excludes subagent
   runs, delegates, and compression continuations by default; the
   bare GROUP BY counted all rows, inflating source counts.

Aggregate shape (exclude_children/include_archived/limit params)
adapted from closed duplicate #61120 by @mijanx.

Closes #48914
Co-authored-by: mijanx <mijanx@users.noreply.github.com>
2026-07-28 19:07:28 +05:30
Soju06
82e2c9ce40 perf(agent): reuse the per-request OpenAI wire client across sequential LLM calls
Every LLM call built a fresh openai.OpenAI wire client (new httpx pool,
TCP+TLS handshake, measured 19.2ms p50 / 35.5ms p95 per call at ~5 calls
per tool-loop turn) and closed it when the request finished. Cache ONE
reusable wire client on the agent, keyed by the effective client kwargs:

- _create_request_openai_client hands back the cached client when the
  effective kwargs are identical; any change (credential rotation,
  provider failover, vision default_headers) evicts and rebuilds.
- Only a request that produced a response reports a reuse close reason
  (request_complete / stream_request_complete); error unwinds report
  *_error_cleanup and really close, so a retry after a request error
  always builds a fresh pool.
- Cross-thread aborts poison the slot: a pool whose sockets were
  shutdown(SHUT_RDWR) from a stranger thread is never reused (#29507) —
  the owner-thread close discards it and the next create rebuilds. The
  holder read and the abort are atomic (under the holder lock) at all
  three abort sites, so a late abort can never poison the NEXT request's
  checked-out client.
- Worker-side interrupt breaks close the half-read SSE stream on the
  owning thread before building the partial response; a failed close
  poisons the slot (otherwise each interrupt leaked one checked-out
  connection until the pool hit PoolTimeout). run_codex_stream gets the
  same poison-on-close-failure handling.
- Single checked-out slot (in_use): a concurrent call gets an untracked
  client with the old per-request lifecycle.
- release_clients() / close() really close the cached client when idle;
  if a worker has it checked out they abort the sockets and detach the
  slot, deferring the FD release to the worker's own close.
- MoA facade and Mock passthroughs never enter the cache; max_retries=0
  is preserved on all request clients.
2026-07-28 19:06:59 +05:30
kshitijk4poor
a41dc65ba7 test: guard coalescing field lists against update_token_counts drift
Follow-up to PR #64171. The _TOKEN_DELTA_* classification must exactly
cover update_token_counts' keyword surface: an unclassified kwarg is
silently kept only from the first delta of a merged run. Introspect the
live signature and fail with a pointed message when a future kwarg is
added without classification (or a classified field is removed).
2026-07-28 18:47:54 +05:30
kshitijk4poor
927633272f fix: claim busy before clearing queue in _stop_token_writer drain
Follow-up to PR #64171. The writer loop and flush_token_counts'
caller-drain both set _token_writer_busy BEFORE popping the queue —
that ordering is what makes flush's lock-free fast path (reads queue
then busy, no cond held) sound. _stop_token_writer's leftover drain did
it backwards (clear queue, then set busy), leaving a few-bytecode window
where a concurrent flush could observe 'empty and idle' and return True
with the popped batch still unapplied. Shutdown-only staleness, no data
loss — but the protocol now matches at all three drain sites.

Test: concurrent flush during a stop-drain mid-apply must time out
(False), never report drained.
2026-07-28 18:47:54 +05:30
kshitijk4poor
e49705d6af fix: respawn dead token writer and never let coalescing kill it
Follow-up to PR #64171. Two writer-death hardening gaps:

- queue_token_counts only spawned the writer when the thread object was
  None, so a writer that died from an unexpected exception could never be
  replaced: deltas piled up on the uncapped deque until a reader's flush
  drained them synchronously. Respawn on 'not thread.is_alive()' instead.
  (atexit re-registration on respawn is safe: unregister removes all equal
  bound-method registrations and the drain hook is idempotent.)

- _coalesce_token_deltas ran outside the per-delta try/except in
  _apply_token_batch, so a merge bug (e.g. an unclassified future kwarg
  summing None + 0) would escape and kill the writer thread. Wrap it and
  fall back to applying the raw batch — coalescing is an optimization,
  never load-bearing.

Tests: coalesce-failure fallback + dead-writer respawn.
2026-07-28 18:47:54 +05:30
Soju06
174ad45939 perf(state): apply per-call token accounting on a background single-writer queue
Every API call in the tool loop persisted its token/cost delta by
calling SessionDB.update_token_counts() synchronously on the turn
thread — a BEGIN IMMEDIATE sessions UPDATE plus a session_model_usage
upsert, measured in production at p50 3.3ms / p95 70.4ms per call and
up to 299ms against a cold multi-GB state.db. The tool loop stalls for
that long between calls, N times per multi-tool turn.

SessionDB gains queue_token_counts(): same signature and semantics as
update_token_counts(), but the critical path is a deque append plus a
condvar notify. A lazily started daemon thread applies deltas in
enqueue order through the existing update_token_counts ->
_execute_write path, so the established self._lock / BEGIN IMMEDIATE /
jitter-retry discipline is unchanged. When a backlog forms, adjacent
same-route incremental deltas coalesce into one UPDATE: token and
api-call fields sum, cost fields sum None-preservingly (an all-None
run stays None so COALESCE keeps the stored value), and absolute=True
deltas never merge and act as ordering barriers. Route equality is
required for a merge because those fields feed COALESCE backfill, the
last-non-None-wins status fields, and the per-model usage attribution
key — a merged apply is row-equivalent to sequential applies.

Correctness and durability:

- flush_token_counts() gives read-your-writes to token/cost readers
  (get_session, list_sessions_rich, _get_session_rich_row,
  list_gateway_sessions, InsightsEngine.generate) — a plain attribute
  check when nothing is queued. The writer sets its busy flag before
  popping the queue so the lock-free fast path can never miss an
  in-flight batch.
- update_session_model / update_session_billing_route /
  update_session_meta write the sessions row synchronously, bypassing
  the queue, so they flush it first: a still-queued first-of-session
  delta carries the pre-switch route, and applying it after the switch
  UPDATE would trip the first_accounted_route branch (api_call_count
  == 0 plus a route mismatch) and resurrect the old model/provider.
- AIAgent._persist_session flushes at turn finalize and every
  error-exit persist point; close() stops and drains the writer before
  the WAL checkpoint; an atexit hook (registered on first enqueue,
  unregistered on close so closed instances are not pinned until
  interpreter exit) drains at shutdown. Worst-case crash loss is the
  in-flight call's delta — the same window as the old inline write.
- A flush trusts a live stop-flagged writer (its loop drains before
  exiting) and only drains on the caller's thread when the writer is
  dead or never started, claiming the same busy flag so concurrent
  flushes wait instead of racing an in-flight batch.
- After close() has stopped the writer, queue_token_counts applies the
  delta inline instead of parking it on a queue nothing will drain; a
  closed-connection failure then raises at the call site, which
  already guards for it, exactly like the old synchronous path.
- Writer apply failures are logged and never raise into a turn; the
  writer thread survives and keeps applying.

Call sites switched to the queue: the per-call site in
agent/conversation_loop.py and both codex app-server sites in
agent/codex_runtime.py. In-memory per-turn counters
(agent.session_estimated_cost_usd etc.) stay synchronous, so live turn
displays never see the queue.

Tests: tests/agent/test_async_token_accounting.py (19 tests: enqueue
ordering, absolute-as-barrier, backlog coalescing with exact sums,
coalesced-vs-sequential row equivalence, merge unit rules, None-cost
preservation, read-your-writes, flush vs stop-flagged/concurrent
drains, inline apply after writer stop, close/atexit durability,
_persist_session drain, writer failure isolation);
tests/run_agent/test_token_persistence_non_cli.py updated to the
queue_token_counts contract.
2026-07-28 18:47:54 +05:30
kshitijk4poor
b259668cac fix: rewrite recover_pending_to_db to use SessionDB.append_message
Critical fixes to salvaged PR #73020:
- Use SessionDB.append_message instead of raw INSERT INTO messages.
  The original used wrong column names (session_key/created_at vs
  session_id/timestamp) and bypassed FTS indexing, session metadata
  updates, display_kind, and all other columns append_message handles.
- Use get_hermes_home() instead of hardcoded Path.home()/'.hermes'.
  Profile-aware path resolution under HERMES_HOME override and active
  profile isolation.
- Add 11 tests covering flush, recovery, serialisation, edge cases.
2026-07-28 18:08:24 +05:30
HexLab98
f134f8cac6 test(agent): cover mount-pool interrupt abort and zero-socket warning 2026-07-28 18:07:44 +05:30
kshitijk4poor
bd1c782456 fix: fire-and-forget read receipts, add docs (#70340 salvage)
- Change await self._send_read_receipt to asyncio.create_task to avoid
  blocking message dispatch on slow bridge responses (matches BlueBubbles
  pattern). Up to 5s per-message latency eliminated.
- Update tests: assert_called_once_with instead of assert_awaited_once_with
  since the receipt is now scheduled, not directly awaited.
- Add send_read_receipts documentation to whatsapp.md following the
  BlueBubbles docs pattern.
2026-07-28 18:02:51 +05:30
Cyrus
652d858f2e fix(whatsapp): apply read receipts after intake policy 2026-07-28 18:02:51 +05:30
Cyrus
35afa8ce06 feat(whatsapp): support inbound read receipts 2026-07-28 18:02:51 +05:30
kshitijk4poor
560800f3cc refactor: salvage follow-ups for PR #59177
- Replace _load_cron_jobs_for_config_warning with lazy import of
  cron.jobs.load_jobs — picks up BOM handling, corruption repair,
  and context-local store resolution for free
- Re-add model.name to axis mapping (was dropped during merge);
  model.name is a legacy alias for model.default
- Fix grammar: '1 enabled unpinned cron job have' -> 'has'
- Pass user_config to cron_model_drift_guard_enabled in set_config_value
  to avoid a redundant load_config() re-read of the file just written
2026-07-28 17:52:21 +05:30
doncazper
3a358cb56b fix(cron): warn before model config changes trip cron drift guard
When an operator changes the global model/provider config, warn that
unpinned cron jobs with stored snapshots will fail-closed on their next
run. Adds a cron.model_drift_guard config opt-out (default true) for
fleets that should deliberately track changing global defaults.

Addresses #59031. Original PR #59177 by @doncazper.
2026-07-28 17:52:21 +05:30
Gille
ff12b62e82 fix(gateway): await hygiene prompt restore 2026-07-28 17:43:28 +05:30
Gille
76a17046e2 fix(gateway): preserve memory prompt during hygiene compression 2026-07-28 17:43:28 +05:30
Booker
1dfe781edd
fix(skills): avoid redundant bind-mount scans (#72622)
Skip hashing active copies when the bundled origin is unchanged, build rename and optional migration indexes lazily, and reuse one optional-skill directory index per sync.

Add regression coverage for unchanged copies, deferred modification detection, lazy rename recovery, and optional provenance scans.
2026-07-28 04:24:50 -05:00
brooklyn!
87a37b9492
Merge pull request #73247 from NousResearch/bb/skill-chip-only
Render a /skill turn as its invocation, never the skill body
2026-07-28 04:22:07 -05:00
Brooklyn Nicholson
dddfe6e4a3 test(gateway): expect display on skill-bundle slash payloads
command.dispatch / slash.exec now project a display invocation for
bundle sends; update the protocol assertions to match.
2026-07-28 04:12:48 -05:00
brooklyn!
dcc543af9a
Merge pull request #73172 from NousResearch/bb/featured-models
feat(picker): curated model defaults + collapsible providers + select-all in Edit Models
2026-07-28 04:09:46 -05:00
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
bf0871fbf8 fix(agent): type a synthesized user turn when its row is written
The auto-continue recovery note was typed only after run_conversation
returned, so its row sat untyped for the whole turn — and permanently
when the continuation was itself killed, which is the case it exists
for. persist_user_display_kind stamps the type on the live message
before the crash persist writes it, in the same insert as the content.
The flush also carries display_metadata through, which it was dropping.
2026-07-28 03:54:16 -05:00
Brooklyn Nicholson
d4449c47e2 feat(picker): derive a newest-per-lab featured shortlist from models.dev
Aggregator providers (nous, openrouter) serve dozens of models across many
labs. Add a featured=True enricher to build_models_payload that attaches a
featured_models shortlist to each row: within every lab keep the newest
_FEATURED_PER_LAB models by models.dev release_date, ranked among that row's
own models — never against the current date, so the choice is stable as
models age. Same-date ties fall back to curated list order. Single-lab
providers get an empty list. Derived live from the models.dev catalog already
loaded on this path; no hand-maintained allowlist.
2026-07-28 03:52:33 -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!
4da7b9ee02
Merge pull request #73169 from NousResearch/bb/queue-drain-semantics
fix(gateway): a queue drain never becomes a live-turn correction
2026-07-28 01:58:31 -05:00
Brooklyn Nicholson
ab68c5efec fix(gateway): a queue drain never becomes a live-turn correction
The desktop's queue promises "run this AFTER the current turn", but the
promise broke on a race the user can't see: a drain that fired when the
client observed idle while the server was still unwinding the turn landed
in _handle_busy_submit, which applied busy_input_mode — redirecting or
interrupting the live turn with text the user explicitly queued. That's
why force-sending the queue felt like a dice roll: the same gesture
steered, interrupted, or queued depending on a millisecond settle race.

prompt.submit now carries queued:true on every fromQueue drain (composer
auto-drain, send-now, background drain), and the gateway's busy path
honors it by forcing queue semantics — never steer, never redirect,
never interrupt. Lose the race and the text simply waits its turn, which
is what queueing meant all along.

Covered on both sides: a gateway test proving a queued drain cannot touch
redirect/steer/interrupt on the live agent, and the desktop drain tests
now assert the flag rides every prompt.submit shape (direct, background,
resume-retry).
2026-07-28 01:48:08 -05:00
brooklyn!
e95ce5a0e1
Merge pull request #73146 from NousResearch/bb/steer-transcript-scaffolding
fix(agent): keep interrupt-checkpoint scaffolding out of the steered transcript
2026-07-28 01:36:15 -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
Brooklyn Nicholson
1eb5ee1eaa fix(mcp): make Figma remote OAuth work via DCR allowlist defaults
Figma's mcp.figma.com register endpoint is a client_name allowlist
(Claude Code / Codex succeed; Hermes Agent 403s) and returns a client
secret while advertising auth_method=none, then requires the secret on
token exchange. Auto-set client_name + client_secret_post for Figma
hosts, pass oauth cfg through login/add paths, force interactive OAuth
for hermes mcp login from non-TTY desktop shells, and ship a catalog
entry. Proven: hermes mcp login figma → 26 tools.
2026-07-28 00:53:16 -05:00
brooklyn!
0ae2997345
Merge pull request #73110 from NousResearch/bb/composer-at-chip
Chip @ file/folder refs instead of dropping them as plain text
2026-07-27 23:46:14 -05:00
Teknium
fab4c888ae feat(voice): say 'stop' to end a voice chat hands-free
Saying EXACTLY a configured stop phrase (default: 'stop') and nothing
else now ends the voice conversation instead of being sent to the agent
as a prompt. Match is deliberately strict — whole utterance,
case-insensitive, surrounding punctuation stripped — so 'stop doing
that and try X' still reaches the agent.

- tools/voice_mode.py: is_voice_stop_phrase() + voice.stop_phrases
  config loader (default ['stop'], [] disables, malformed config falls
  back safely).
- hermes_cli/voice.py: shared continuous loop (TUI + desktop) halts on
  a stop phrase exactly like the silent-cycle limit (fires
  on_silent_limit so every UI turns voice off); stop_continuous
  force-transcribe path swallows the phrase without counting a silent
  cycle.
- cli.py: classic CLI push-to-talk/continuous path and barge-in
  utterance path disable voice mode on a stop phrase.
- Config default voice.stop_phrases: ['stop']; docs updated.

Tests: tests/tools/test_voice_stop_phrase.py (27 tests,
sabotage-verified: loop test fails when detection is disabled);
voice suites green (110 + 44 passed).
2026-07-27 21:26:23 -07:00