Commit graph

13683 commits

Author SHA1 Message Date
georgex8001
62b9fb6623 fix(acp): thread-safe interactive approval via contextvars
Concurrent ACP sessions run on a shared ThreadPoolExecutor (max_workers=4).
Each _run_agent mutated the process-global os.environ["HERMES_INTERACTIVE"]
and restored it in finally, so one session's restore could clobber another's
set mid-run — dropping the second session onto the non-interactive
auto-approve path, executing a dangerous command without the approval
callback firing (GHSA-96vc-wcxf-jjff).

Replace the env-var flag with a thread/task-local contextvar in
tools.approval. The two HERMES_INTERACTIVE read sites in approval.py now go
through _is_interactive_cli() (contextvar-first, env fallback for legacy
single-threaded CLI callers). The ACP executor sets the contextvar instead
of os.environ; the existing contextvars.copy_context() wrapper isolates each
session's write.

Co-authored-by: Hermes Agent <127238744+teknium1@users.noreply.github.com>
2026-06-30 03:24:58 -07:00
teknium1
f5eb4c307b fix(gateway): stop Matrix upload fallback from leaking host path
The Matrix adapter's _upload_file fell back to sending
"(file not found: {file_path})" directly into the room — the same
host-path leak class fixed for the base adapter and Slack in the
previous commit. Replace it with a friendly notice, log the path at
WARN for operators, and preserve any caller-supplied caption.
2026-06-30 03:24:36 -07:00
UgwujaGeorge
cb9d18c759 fix(gateway): stop media-send fallbacks from leaking host paths into chat
The base BasePlatformAdapter implementations of send_voice, send_video,
send_document, and send_image_file forwarded their *_path argument
verbatim into the chat text (e.g. "🎬 Video: /home/.../hermes/cache/...").
Telegram, Discord, and Slack adapters all fall back to those base methods
when their native send raises — so a rejected video on Telegram surfaced
the host filesystem layout to the user instead of a useful message.

Replace the path-echo with a friendly notice, log the path for operator
diagnostics, and keep the user-supplied caption intact. The Slack adapter
had three identical sites that fell through to the same path-echo on its
own native upload failures; fix those too. send_document still surfaces
the caller-provided file_name (or the basename derived from it) since
that is the user-facing filename, not a host path.

Add regression tests asserting the *_path argument never appears in the
fallback content while caption text and explicit file_name still do.
2026-06-30 03:24:36 -07:00
teknium1
fee3d4ed04 test(gateway): update startup-restart-race fixtures for current main
The salvaged test double predated two main changes:
- start() now connects via _connect_adapter_with_timeout, which forwards
  is_reconnect to adapter.connect(); the StartupRaceAdapter double didn't
  accept the kwarg.
- stop() now awaits _finalize_shutdown_agents (async on main); the fixture
  stubbed it as a plain MagicMock.

Accept is_reconnect in the double and use AsyncMock for the finalize stub.
2026-06-30 03:22:18 -07:00
Disaster-Terminator
f4a54b6292 fix(gateway): abort startup during restart 2026-06-30 03:22:18 -07:00
Kartik
c6eb7f9e72
fix(memory/mem0): recall on the current question + stronger search guidance (#55535) 2026-06-30 15:51:08 +05:30
Tao Yan
b8ebe32866 fix(agent): flatten multi-part user_message in codex intermediate-ack detector
Vision requests routed through the OpenAI-compat API server forward the
raw multi-part content list ([{type:"text"}, {type:"image_url"}, ...])
straight through as user_message. The codex intermediate-ack detector
flattened it with (user_message or "").strip(), so a truthy list survived
and .strip() raised AttributeError — killing any Codex-routed vision turn
that took the require_workspace path.

Route through the existing _summarize_user_message_for_log helper (which
already backs the logging/banner previews on main), and widen the param
type hint from str to Any to match how the function is actually called.

The two logging-preview sites the original PR also touched were fixed
independently on main by the conversation-loop refactor.

Co-authored-by: Hermes Agent <agent@nousresearch.com>
2026-06-30 03:20:11 -07:00
Markus Phan
cd9f5cc671 fix(delegate): route subagent progress lines through _safe_print for ACP stdio
delegate_task's per-task completion display emitted lines like
"✓ [1/3] Research done (17.92s)" via a bare print(). Under ACP (and any
headless JSON-RPC stdio host where AIAgent routes human output to stderr
via a custom _print_fn), these landed on stdout and corrupted the
protocol frame stream, surfacing as "Failed to parse JSON message: ✓
[3/3] …" in the ACP adapter.

Add _emit_parent_console() which prefers parent_agent._safe_print (the
same hook AIAgent uses for every other user-facing print) and falls back
to print() only when no router is wired up or it raises. CLI behavior is
unchanged.

The PR's other fix (preset toolset expansion) is already covered on main
by _expand_parent_toolsets(), so only the stdio-safe printing change is
salvaged here.
2026-06-30 03:16:22 -07:00
teknium1
eeb4735078 test(web_server): assert ws-ping invariant, not frozen 20.0 literal
The loopback ws-ping window is now 30s/60s (#48445/#50005), so the
hardcoded == 20.0 assertion was a change-detector that broke the moment
the loopback tuning landed. Assert the behavioral contract instead: ping
stays enabled (positive) and timeout >= interval.
2026-06-30 03:11:13 -07:00
teknium1
db880186f2 chore(release): add AUTHOR_MAP entries for #51841 and #54287 salvage 2026-06-30 03:11:13 -07:00
teknium1
1a0c576813 fix(tui_gateway): drop emit-only session.info from _LONG_HANDLERS
session.info is only ever an emitted event (_emit), never a dispatched
@method RPC, so listing it in _LONG_HANDLERS is dead weight that can
never match a dispatched method name. Remove it from the set and the
test's frontend-polled list to keep _LONG_HANDLERS to real RPCs.
2026-06-30 03:11:13 -07:00
Zyxxx-xxxyZ
9d10dcd490 fix(tui_gateway): route frontend-polled inline RPCs to pool under GIL pressure
Frontend-polled read-only RPCs (session.list, pet.info, process.list)
ran inline in the WS read loop. Under GIL pressure from concurrent agent
turns they block the loop, timing out frontend polls and surfacing as a
false "needs setup" / dropped session (#50005, #48445). Route them
through _LONG_HANDLERS so dispatch() returns immediately, and raise the
default RPC pool to 8 workers so the added long handlers don't queue.

Co-authored-by: Hermes Agent <noreply@nousresearch.com>
2026-06-30 03:11:13 -07:00
Peetwan
ebb81f10cb fix(tui_gateway): prevent WS disconnect under GIL pressure
Three targeted fixes for Desktop GUI WebSocket stability when agent
turns starve the uvicorn event loop of CPU (GIL contention):

1. Loosen ws_ping_timeout for loopback binds (QW-1)
   - Loopback (Desktop): ping 30s interval / 60s timeout
   - Non-loopback (Cloudflare Tunnel): unchanged 20/20
   - A GIL-heavy agent turn can stall the event loop past 20s;
     uvicorn's keepalive ping runs on that same starved loop, so a
     20s timeout kills an otherwise-healthy local connection over a
     recoverable stall. 60s rides out the stall without affecting
     half-open detection on public binds.

2. Coalesce streaming token frames in WSTransport (CF-2)
   - Buffer high-frequency delta frames (message.delta, reasoning.delta,
     thinking.delta) and flush as a batch every ~33ms (~30fps)
   - Non-streaming frames (RPC responses, control/tool/completion events)
     flush pending tokens first — wire ordering preserved
   - Thread-safe via threading.Lock; worker threads return immediately
     instead of blocking on per-token loop wakeups
   - Reduces event-loop wakeup churn by orders of magnitude during model
     streaming, directly cutting GIL pressure

3. Loop heartbeat watchdog (CF-1)
   - Self-rearming call_later tick (2s) measures drift between expected
     and actual fire time using loop.time() (monotonic)
   - Logs 'event loop stalled Ns (GIL pressure suspected)' when drift >5s
   - Turns mysterious WS drops into diagnosable log entries
   - Uses call_later chain (not a task) — dies with the loop, nothing
     to cancel on shutdown

Root cause: uvicorn's ws keepalive ping (20/20s) runs on the same
starved event loop as agent turns. Under GIL pressure from heavy agent
turns or delegation, the loop can't service the ping within 20s, so
the websockets protocol declares the connection dead. Reconnects fail
with ready_send_failed because the old process's loop is still wedged.

None of these fixes touch the model-facing message array, prompt
caching, message role alternation, or the wire protocol — they are
strictly display-transport improvements plus a config tweak and a
diagnostic log.

Tests: 762 passed, 17 skipped (0 failures) across test_tui_gateway_ws,
test_tui_gateway_server, test_web_server, and tui_gateway/ suites.
2026-06-30 03:11:13 -07:00
teknium1
35a0803a3b fix(delegation): budget subagent summaries against parent context headroom
Batch delegation returned each subagent's full final_response verbatim
into the parent's context. A fan-out of N children could dump 60k+ tokens
at once, blowing the parent's context window and — on rate-limited
providers — triggering a compression/429 death spiral (429 misread as
context-too-large -> window step-down -> retry loop -> conversation dies).

Cap each summary against the parent's *remaining* context headroom split
across the batch (not a magic char count). When trimming, mirror the
web_extract convention: spill the full text to cache/delegation (mounted
into remote backends via credential_files._CACHE_DIRS) and return a
head+tail window (75/25, line-snapped) plus a footer with the exact
read_file offset to page the omitted middle. Both the subagent's opening
AND its closing (outcomes / files-changed / issues, which live at the end)
survive in-context, and nothing is lost — the parent can read_file the
full version on any backend.

delegation.max_summary_chars (default 24000) is a static ceiling layered
on top as belt-and-suspenders for models that ignore 'be concise'; 0
disables it. Child prompt tightened to lead with outcomes / bullets.

Co-authored-by: rc-int <rcint@klaith.com>
2026-06-30 03:07:40 -07:00
MarioYounger
3b2bb30c5d fix(security): harden heredoc approval, NFKC homograph fold, env-var filter
Three independent security-scanner hardenings, re-homed onto the current
shared threat-pattern architecture (tools/threat_patterns.py):

- approval.py: add bash/sh/zsh/ksh heredoc to DANGEROUS_PATTERNS. The
  existing heredoc pattern only covered python/perl/ruby/node, so
  `bash <<'EOF' ... EOF` ran arbitrary shell — including exfil pipelines
  whose inner commands don't individually match a pattern — with no prompt.

- threat_patterns.py: apply unicodedata.normalize("NFKC", ...) before
  pattern matching so full-width / compatibility homographs (e.g.
  `cat ~/.hermes/.env`) are folded to ASCII and no longer bypass the
  keyword scanners. Invisible-char detection still runs on the raw content
  first (NFKC can strip those codepoints).

- code_execution_tool.py: add CREDS/BEARER/APIKEY to _SECRET_SUBSTRINGS so
  vars like HERMES_LLM_CREDS, API_BEARER, MY_APIKEY are scrubbed from the
  sandbox env. PASS was intentionally dropped from the original proposal —
  it false-positives on BYPASS_CACHE / COMPASS_DIR / PASSENGER_HOST while
  PASSWORD/PASSWD already cover the credential cases.

The original PR also proposed a 'synonym' injection pattern block
(overlook/forget/set aside/bypass/discard + developer-mode); dropped here
because it false-positives on ordinary AGENTS.md/SOUL.md prose ("don't
forget to follow the rules", "run in developer mode"), exactly the
bossy-English class threat_patterns.py is documented to avoid.

Salvaged from #9028.

Co-authored-by: Hermes Agent <agent@nousresearch.com>
2026-06-30 02:59:46 -07:00
Teknium
c8376e0dc6
fix(auxiliary): stop SDK retries from multiplying compression stall (#54465) (#55544)
The auxiliary OpenAI clients were built without overriding the SDK's
default max_retries=2, so every aux call silently made up to 3 attempts
against a slow/hung endpoint — a 120s timeout could stall ~360s before
Hermes saw a single failure. On the critical compression preflight path,
Hermes then added its own same-provider timeout retry on top, roughly
doubling the user-visible stall again before fallback.

- Build both the sync (_create_openai_client) and async (_to_async_client)
  aux clients with max_retries=0 (setdefault, so explicit callers still
  override). Hermes already owns retry + provider/model fallback policy.
- For task == compression, skip the same-provider transient retry on a
  full-budget timeout and fall straight through to fallback. Fast blips
  (streaming-close, 5xx) still retry, since those are cheap.
- Add _is_timeout_error to distinguish a full-budget timeout from a fast
  connection drop.

Addresses the retry-multiplication root cause of #54465 (the resume-wedge
persistence half landed in #55499).
2026-06-30 02:54:08 -07:00
0xbyt4
e6f66bc0f0 fix(security): cover Move and no-space headers in patch_tool sensitive path check
patch_tool extracts V4A patch paths so _check_sensitive_path can refuse
writes to /etc/*, /boot/*, etc. before they reach the low-level file ops.
The extraction regex had two gaps:

1. `*** Move File: src -> dst` was never extracted (regex only matched
   Update/Add/Delete), so a Move targeting /etc/crontab skipped the
   pre-check and fell back on the narrower file_operations deny list.
2. The regex required `\\s+` after `***` but patch_parser uses `\\s*`, so
   `***Update File: /etc/hosts` (no space) parsed + applied while
   skipping the check.

Loosen the leading whitespace to \\s* and add a Move regex that checks
both endpoints. Move endpoints also run through the same '..' traversal
rejection as the other V4A headers (closes the sibling gap on current
main, which gained that traversal guard after this PR was opened).
2026-06-30 02:50:24 -07:00
kshitij
26f39f7b90
fix(credentials): prefer ~/.hermes/.env over stale os.environ on key rotation (#55528)
`_resolve_api_key_provider_secret` resolved API keys via `get_env_value`,
which returns the `os.environ` value first and only falls back to
`~/.hermes/.env`. After a user rotates a key in `.env`, a stale value still
exported in the parent shell (Codex CLI, test runner, login profile) shadows
the fresh key on every request, producing persistent 401s.

The credential-pool seeding path was already fixed to prefer `.env`
(#18254/#18755), but the live request-time resolution path was not — so the
pool re-seeded with the fresh key while `_resolve_api_key_provider_secret`
kept returning the stale shell export. This closes that remaining path.

- config: add `get_env_value_prefer_dotenv()` — checks `~/.hermes/.env`
  first, then `os.environ`. Distinct from `get_env_value()` (unchanged,
  os.environ-first) so only Hermes-managed credential resolution flips
  precedence; the generic helper's many callers are unaffected.
- auth: `_resolve_api_key_provider_secret` resolves through the new helper.
- tests: regression coverage for both the pool-seeding path and the
  auth resolution path (a rotated `.env` key must beat a stale shell export).

Closes #20591.

Co-authored-by: 0xDevNinja <manmit0x@gmail.com>
2026-06-30 09:49:52 +00:00
teknium1
b6045170bb fix(discord): extend channel-name matching to slash-command auth; clamp flush deadline to disconnect budget
Follow-up to the salvaged #8008 fix:
- Sibling-site fix: _evaluate_slash_authorization gated DISCORD_ALLOWED_CHANNELS /
  DISCORD_IGNORED_CHANNELS on numeric IDs only, so name/#name config that now works
  for on_message still silently failed for slash-command interactions. Refactor the
  channel-key helper to _discord_channel_keys_from_channel(channel, parent) and reuse
  it at the interaction gate. Fail-closed on missing channel id is preserved.
- The contributor's hardcoded 8s flush deadline could be hard-cancelled mid-flush:
  _teardown_adapter already wraps cancel_background_tasks() in the per-adapter
  disconnect budget (HERMES_GATEWAY_ADAPTER_DISCONNECT_TIMEOUT, default 5s). The flush
  deadline now derives from that budget with headroom so it always completes inside it.
- AUTHOR_MAP: map cypher@augmentl.com -> Nickperillo for CI.
- Tests: slash-auth name/#name allow + name ignore matching.
2026-06-30 02:48:42 -07:00
Cypher
cb9308f0a6 fix(discord): channel name matching and flush pending sends on shutdown
Two related fixes to the Discord gateway adapter:

1. Channel name matching (free-response, allowed, ignored, no-thread channels)
   Previously these config values only matched against numeric channel IDs.
   If a user configured free_response_channels: cypher (by name), the adapter
   would silently ignore it because it only intersected against channel_ids.
   Now the adapter builds a channel_keys set that includes the channel ID,
   channel name, and #channel-name form, and checks all three for each gate.

2. Flush pending text-batch tasks before shutdown
   The Discord adapter uses _pending_text_batch_tasks (its own dict) for
   merging rapid successive message chunks. These tasks were NOT added to
   self._background_tasks (the base class list), so the base
   cancel_background_tasks() never awaited them on restart/shutdown.
   This caused a race: in-flight response deliveries were cancelled before
   Discord had a chance to send them, resulting in silent dropped messages
   visible to users as tool-log-only replies with no text body.

   Fix: override cancel_background_tasks() in DiscordAdapter to await all
   pending text-batch tasks (8s deadline) before delegating to the base class.
2026-06-30 02:48:42 -07:00
Teknium
b03635daea
fix(approval): catch hermes gateway stop/restart behind a profile flag (#55515)
The gateway-lifecycle guard's hermes-CLI pattern required `hermes`
and `gateway` to be adjacent, so a profile flag slipped the agent
past it: `hermes -p ade gateway restart` was not flagged. That is the
exact form from the 2026-04-11 ade-profile self-kill loop. Allow an
optional run of global flags (`-p ade`, `--profile ade`, multiple
flags) between `hermes` and the gateway subcommand.

launchctl self-termination is already covered on main by #33071; this
narrows the only remaining real gap.
2026-06-30 02:48:30 -07:00
brooklyn!
1d495cfbbf
Merge pull request #55226 from NousResearch/bb/desktop-memory-graph
feat(desktop): memory graph — playable timeline of memories + skills over time
2026-06-30 04:36:17 -05:00
brooklyn!
6d20ac4c85
Merge pull request #55500 from NousResearch/bb/desktop-composer-draft
perf+refactor(desktop): de-entangle the composer into isolated engine hooks
2026-06-30 04:35:28 -05:00
Brooklyn Nicholson
aa07400e1a chore(desktop): keep draft persist effect deps clean
Replace direct queueEditRef reads in cleanup/pagehide with a mirrored local ref so hook deps stay stable and eslint-clean.
2026-06-30 04:33:08 -05:00
Brooklyn Nicholson
9998ff4cbe fix(desktop): persist live composer draft before swap/reload
Sync the contentEditable text before stash-on-scope-change and pagehide so pending rAF draft flushes cannot drop the newest keystrokes.
2026-06-30 04:32:39 -05:00
brooklyn!
eeb69c7df2
Merge pull request #55547 from NousResearch/bb/54744-windows-bash-spawn
fix(desktop): tree-kill Windows terminal descendants
2026-06-30 04:28:33 -05:00
Brooklyn Nicholson
2f46fde3f5 fix(desktop): keep queued composer edit ref in sync
Update the shared queued-edit ref synchronously with React state so draft persistence sees the correct edit mode while loading and restoring queued prompts. Also drop the accidental node_modules symlink from the PR.
2026-06-30 04:27:22 -05:00
Brooklyn Nicholson
e5253d852b fix(desktop): tree-kill Windows terminal descendants
Ensure Windows desktop and local terminal teardown kill full process trees so Git Bash descendants cannot survive wrapper exits and accumulate across retries.
2026-06-30 04:23:27 -05:00
Brooklyn Nicholson
94d70dee54 perf(desktop): stop ChatBar re-rendering on cross-session status/queue churn
Audit follow-up. ChatBar subscribed to the whole `$statusItemsBySession` (a
computed that rebuilds the entire map) + `$previewStatusBySession` maps just to
derive a boolean, so every per-item status mutation (a subagent tick, the 5s
background poll) and every OTHER session's change re-rendered the ~1.4k
component. The queue hook likewise subscribed to the whole `$queuedPromptsBySession`
map.

- Add `useSessionStatusPresence` — a coarse edge (useSyncExternalStore) that
  flips only when the stack shows/hides; ChatBar uses it for the styling
  data-attr instead of the two map subscriptions.
- Add generic `useSessionSlice(store, key)` — subscribes to one session's array,
  bailing out when other sessions churn (the plain atom keeps per-key refs
  stable). The queue hook now reads its slice through it.

Result: ChatBar re-renders only when the stack's presence flips or this session's
queue changes — not on background/subagent status streaming or other sessions.

Verified: typecheck clean, 0 lint errors, composer tests 39/40 (pre-existing
attachments failure unrelated).
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
33d91029b2 perf+fix(desktop): coalesce composer paste/input flush; scope dock glow to thread
Two composer fixes:

- **Paste/input lag** — `flushEditorToDraft` serializes the whole editor
  (`composerPlainText` is O(n)); running it on every event during a burst
  (holding a key, or holding Cmd+V into a growing editor) was O(n²). Coalesce
  the input/paste path to one flush per animation frame. Lossless: the
  contentEditable DOM is the source of truth and submit + the compositionend /
  keydown paths re-read it synchronously (those stay immediate).
- **Detached-composer dock glow** — was `fixed inset-x-0` (full viewport, spilled
  under the sessions sidebar). Switched to `absolute inset-x-0`, so it anchors to
  the chat-column root the docked composer centers in — the glow now spans only
  the thread area, matching the actual dock target.

Verified: typecheck clean, 0 lint errors, composer DOM repro tests pass.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
773a3703bf refactor(desktop): extract composer submit engine into useComposerSubmit
Lift the submit orchestration out of ChatBar into
composer/hooks/use-composer-submit.ts: `submitDraft` (the one decision tree —
queue-edit save · slash-now-while-busy · queue · drain · send · stop),
`dispatchSubmit` (the shared send-with-restore primitive + the external-submit
listener), and `steerDraft`.

This is the seam where the draft and queue engines meet; it now reads both clean
APIs as explicit inputs instead of closing over inline state. ChatBar is left as
a thin coordinator that owns the shared `queueEditRef` and wires the four engines
(draft · queue · submit · metrics/voice/drop) into render.

Behaviour-identical (verbatim move). Verified: typecheck clean, composer DOM
repro tests (enter-submit, IME, slash-now, steer, drain) pass.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
4c4b790f11 refactor(desktop): extract composer queue engine into useComposerQueue
Lift the queue subsystem out of ChatBar into composer/hooks/use-composer-queue.ts:
the per-session queue-store binding + queuedPrompts, in-place queued-prompt
editing (begin/step/exit), the shared drain lock + send-then-remove sequence,
manual send-now, bounded auto-drain, and the three queue effects (re-key migrate,
idle auto-drain, queue-edit cleanup).

It consumes the draft API (draftRef/clearDraft/loadIntoComposer/focusInput) and
writes the coordinator-owned `queueEditRef` the draft engine reads — so the
draft↔queue coupling is two explicit deps, not an inline tangle. `steerDraft`
and the chat-focus Esc-cancel stay in ChatBar (not queue-internal).

Behaviour-identical (verbatim move). Verified: typecheck clean, composer DOM
repro tests + queue/edit paths pass.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
9ee7333e5b refactor(desktop): extract composer draft engine into useComposerDraft
De-entangle the draft spine: lift the source-of-truth engine (the imperative
composer-runtime subscription, edit primitives, focus, edge selectors, and
per-session load/clear/stash/restore) out of ChatBar into
composer/hooks/use-composer-draft.ts.

The draft↔queue cycle is broken by making `queueEditRef` a coordinator-owned
ref ChatBar threads into the hook (explicit dep, not an implicit shared global).
The contentEditable *event* handlers stay in ChatBar (they bridge into the
trigger engine) and drive the primitives the hook exposes.

Behaviour-preserving (verbatim move); typing perf preserved. Verified: typecheck
clean, composer DOM repro tests (enter-submit, IME, slash-nav) + text-guard pass.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
bd53230739 refactor(desktop): extract composer drag-and-drop into useComposerDrop
Lift the attachment drop engine (dragActive + the 7 drag/drop handlers + the
in-app-ref vs OS-upload split) out of ChatBar into
composer/hooks/use-composer-drop.ts. Self-contained, off the keystroke path —
consumes insertInlineRefs + onAttachDroppedItems + requestMainFocus. Verbatim
move, behaviour-preserving.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
cf05b38683 refactor(desktop): extract composer voice engine into useComposerVoice
Lift the dictation + voice-conversation + auto-speak subsystem out of ChatBar
into composer/hooks/use-composer-voice.ts. It owns voiceConversationActive,
lastSpokenIdRef, the pending-reply readers, submitVoiceTurn, the voice
hooks (recorder/conversation/auto-speak), the Ctrl+B toggle event, and
handleToggleAutoSpeak; it exposes dictate/voiceStatus/voiceActivityState/
conversation/start+endConversation/handleToggleAutoSpeak for the controls.

Self-contained: consumes the draft/submit primitives (insertText, clearDraft,
focusInput, onSubmit) passed in, nothing depends back on it — so unlike the
queue subsystem (which is circularly coupled to the draft helpers) it lifts
cleanly. Behaviour-preserving; verbatim move.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
00694b935f perf(desktop): composer typing no longer re-renders ChatBar (imperative draft sync)
The real composer state-engine fix. ChatBar subscribed to the full draft string
(`useAuiState(s => s.composer.text)`), so every keystroke re-rendered the whole
~2k-line component even though the contentEditable DOM already owns the text.

Replace that with:
- an imperative composer-runtime subscription (useComposerRuntime().subscribe)
  that mirrors text into draftRef, repaints the editor ONLY on external changes
  (clear/restore/insert; the focused editor is the source otherwise), and drives
  the debounced per-session stash — all without a React render. This folds the
  old `[draft]` sync effect and the `[draft]` debounced-stash effect into one
  place keyed off the runtime, surviving core rebinds via the effect dep.
- coarse edge selectors (hasText / isHelpHint / isSteerableText, plus
  isEmpty / hasHardNewline in useComposerMetrics) for the chrome, which only
  re-render when an edge actually flips.

Net: typing within a line does zero ChatBar re-renders / style invalidations;
work happens only on real edges. Behaviour-preserving — draftRef + editor are
already kept current by every mutation path; verified by the composer DOM repro
tests (enter-submit, IME composition, slash-nav) + text-guard.
2026-06-30 04:19:10 -05:00
Brooklyn Nicholson
e0a78336c1 refactor(desktop): extract composer sizing into useComposerMetrics
First step of decomposing the ChatBar god component (composer/index.tsx). Pull
the self-contained *sizing* engine — stacked/inline layout + the measured-height
CSS vars the thread reads for clearance — into composer/hooks/use-composer-metrics.ts.

The hook owns: the media-query `narrow`, `expanded`/`tight`, the 8px height
bucketing (so per-keystroke growth never invalidates the tree's computed style),
the ResizeObserver, the popout re-sync, and the CSS-var cleanup. ChatBar now
just calls `useComposerMetrics(...)` and consumes `stacked`.

Behaviour-preserving (no keystroke/IME/contentEditable path touched): code moved
verbatim. Deliberately a low-risk first slice on the app's most fragile file;
the draft/state-engine spine is the next, dogfood-heavy step
(see desktop-composer-plan.md).
2026-06-30 04:19:10 -05:00
brooklyn!
90c5433411
Merge pull request #55543 from NousResearch/bb/desktop-status-stack-icons
fix(desktop): proper agent icon for subagents + a queue icon
2026-06-30 04:18:27 -05:00
Brooklyn Nicholson
f47459cdbd fix(desktop): proper agent icon for subagents + give the queue an icon
Two status-stack icon nits:
- Subagents used `hubot`; switch to the dedicated `agent` codicon.
- The queue section had no icon while every other group (todos, subagents,
  background) has one. Give it `layers` (a stack of pending turns), matched to
  the group-icon styling so all four sections read consistently.
2026-06-30 04:16:37 -05:00
brooklyn!
67783ad4e7
Merge pull request #55542 from NousResearch/bb/desktop-preview-stack-visible
fix(desktop): keep composer preview links visible when a background task appears
2026-06-30 04:08:40 -05:00
Teknium
3f19df2a5b
fix(mcp): late-refresh must see desktop/dashboard discovery thread owner (#55514)
MCP tools connected and enabled but never surfaced into the agent's
session toolset on the desktop app + dashboard WebUI (#51587).

There are two independent background MCP discovery thread owners by
surface: tui_gateway.entry (stdio 'hermes --tui') and hermes_cli.mcp_startup
(desktop app + dashboard WS sidecar via tui_gateway/ws.py, and 'hermes
dashboard'). The late-refresh scheduler gates on
tui_gateway.entry.mcp_discovery_in_flight(), which read ONLY the entry
thread global. On the desktop/dashboard surfaces that global is None, so a
server slower than the bounded build-time wait never triggered a late
refresh and its tools stayed invisible for the whole session.

Make mcp_discovery_in_flight() / join_mcp_discovery() consult BOTH thread
owners. Adds the matching in-flight/join helpers to hermes_cli.mcp_startup
and has tui_gateway.entry delegate to them as a second owner.
2026-06-30 02:08:37 -07:00
Brooklyn Nicholson
57462341f4 fix(desktop): keep composer preview links visible when a bg task appears
Preview links (detected HTML files / localhost dev URLs) were rendered as
CHILDREN of the background StatusSection, which is collapsed by default — so the
moment a background task appeared, the previews got swallowed into the collapsed
"N Background" expandable and vanished until you manually expanded it. With no
background group they rendered as a standalone always-visible block, so the bug
only showed once a bg task was running.

Render the preview links as their own always-visible block right after the
background section instead of as collapsible children. They stay visually
associated with the background group (a localhost dev server and its preview are
the same thing) but are no longer hidden by its collapse — a one-tap open is the
whole point.
2026-06-30 04:06:26 -05:00
Brooklyn Nicholson
babbefb164 fix(desktop): scope memory graph cache by profile
Ensure the Memory Graph cannot show stale data after switching profiles, and tighten the graph backend's profile-safe timestamp handling.
2026-06-30 03:44:41 -05:00
nightq
fa3ab2ffd0 fix: normalize tool_call_id whitespace in sanitizer
_sanitize_api_messages() compared raw tool_call_id strings without
stripping whitespace. When assistant-side IDs and tool-result IDs
diverged due to surrounding whitespace, valid tool results were treated
as orphaned and replaced with [Result unavailable] stub placeholders.

Strip whitespace in _get_tool_call_id_static() (both call_id/id paths,
dict and object) and at the two result_call_id comparison sites in
sanitize_api_messages(). Adds regression tests for preserved-whitespace
results and orphaned-whitespace removal.

Closes #9999
2026-06-30 01:43:40 -07:00
Brooklyn Nicholson
3e7ed0c53b feat(desktop): memory-graph share dialog + core/zoom & light-mode polish
- Rework share/import into one Dialog (matches rename/create): a single code
  field (copy to share, paste + Load to import) with a hover copy button, a
  Reset link beside the upload icon when viewing an imported map, and plainer
  copy.
- Core orb: scales with the world zoom (~1.25× the inner shell), backdrop wash
  behind it; on focus/hover the scene composites above the orb so the active
  tooltip + lit lines are never covered.
- fitViewport floors zoom at the reference (5-ring) extent, so big maps render
  at a constant scale and pan instead of shrinking every node to fit.
- Light mode: flip inter-ring band shading to read as depth (not a mound),
  fade the core ring in from t=0, drop the timeline star glow.
- Timeline: filled play glyph, crisper constellation, date moved into the legend.
2026-06-30 03:22:46 -05:00
brooklyn!
f9b619dfae
Merge pull request #55504 from NousResearch/bb/desktop-split-prompt-body
refactor(desktop): decompose use-prompt-actions (slash + submit sub-hooks)
2026-06-30 03:22:43 -05:00
brooklyn!
90f59ecdbb
Merge pull request #55501 from NousResearch/bb/desktop-split-message-stream
refactor(desktop): split use-message-stream (utils + gateway-event sub-hook)
2026-06-30 03:22:21 -05:00
Brooklyn Nicholson
7337248a4c refactor(desktop): extract submit pipeline into use-prompt-actions/submit
After the slash dispatcher, the next-largest body unit was submitPromptText —
a ~280-line submit pipeline. Lift it into a colocated useSubmitPrompt sub-hook
(use-prompt-actions/submit.ts) with a typed SubmitPromptDeps object; body moves
verbatim. SubmitTextOptions moves to utils.ts (shared by submit + submitText).

Pure restructuring, no behaviour change (full use-prompt-actions suite green).
index.ts: 1,212 -> 937.
2026-06-30 03:15:10 -05:00
Brooklyn Nicholson
51a710e57e refactor(desktop): extract gateway-event dispatcher into its own sub-hook
The remaining bulk of useMessageStream was handleGatewayEvent — a ~550-line
event-type dispatcher. Lift it into a colocated useGatewayEventHandler sub-hook
(use-message-stream/gateway-event.ts): the values it closed over (sibling
streaming callbacks + the 3 stable refs the deps array omitted + options)
become a typed GatewayEventDeps object; the dispatcher body moves verbatim.

Pure restructuring, no behaviour change (utils tests still green). index.ts:
1,120 -> 540.
2026-06-30 03:11:14 -05:00
kshitijk4poor
58d8e25e67 fix(agent): make compression lock-lease refresher tolerate transient DB blips
Follow-up hardening on the salvaged #54465 backoff persistence work.

The lease refresher's loop treated ANY falsy refresh as a permanent stop
(`if not refreshed: break`), conflating two distinct cases:
  - genuine lost-ownership (rowcount 0) — correct to stop, and
  - a one-off transient DB error (write contention that escapes
    _execute_write's retry budget) — which returned False identically.

A single transient blip therefore killed the lease for the rest of a
multi-minute compression call, silently reintroducing the exact 300s-TTL <
~361s-call expiry wedge the PR set out to fix.

Changes:
- _CompressionLockLeaseRefresher._run now tolerates a bounded run of
  consecutive failures (_MAX_CONSECUTIVE_REFRESH_FAILURES = 3) before giving
  up the lease; a recovered tick resets the counter. Worst-case extra hold is
  cap * refresh_interval, still bounded by the acquirer's TTL.
- Replace the two remaining silent `except Exception: pass` arms in the
  compression-failure-cooldown persist/clear helpers with debug logging, for
  parity with their sqlite3.Error sibling arms (a non-sqlite bug was invisible).
- Document the join(timeout=1.0) quiesce bound in stop().
- Add 3 regression tests: single-blip tolerance, persistent-failure stop at the
  cap, and refresh-raising tolerance.
2026-06-30 13:36:29 +05:30