Commit Graph

22420 Commits

Author SHA1 Message Date
SHL0MS cbfe186da8 fix(config): canonicalize legacy api_mode spellings instead of silently discarding them
Earlier releases accepted api_mode: openai on custom provider entries.
The canonical transport set is now {chat_completions, codex_responses,
anthropic_messages, bedrock_converse, codex_app_server}, and an
unrecognized value was silently ignored at both consumption sites
(_normalize_custom_provider_entry passes the raw string through and
agent_init's accepted-set check drops it; _parse_api_mode returns None),
falling through to hostname-based detection.

For hosts with a detection rule the provider silently switches
transports after an update. Observed live: a custom entry for
api.actual.inc with api_mode: openai (valid when written) flipped to
codex_responses via the hostname rule, and every reasoning-bearing
request to the relay's /v1/responses failed with a wrapped non-JSON
error while /v1/chat/completions worked throughout.

Fix: one shared alias map (_canonical_api_mode) consulted by both
sites. openai/openai_chat -> chat_completions, responses ->
codex_responses, anthropic/messages -> anthropic_messages, bedrock ->
bedrock_converse. Canonical names and unknown values pass through
unchanged, so invalid-config behavior is untouched.

Tests: alias map contract (every alias lands in _VALID_API_MODES),
normalizer canonicalization incl. the transport: key alias, and the
runtime gate accepting legacy spellings while still rejecting unknowns.
2026-08-14 16:36:39 -07:00
Teknium f92accd05d fix(tui): lead slash_worker PATH with Hermes' managed bin dir
Follow-up on the #83854 salvage: prepend $HERMES_HOME/bin ahead of the
venv and user-local bin dirs, matching the managed-first Browser Use
CLI resolution policy — the worker resolves the same canonical binary
the agent process does.
2026-08-14 16:22:47 -07:00
webtecnica 73b4e41ab2 fix(tui): prepend Hermes venv and user-local bin to slash_worker PATH (#83845) 2026-08-14 16:22:47 -07:00
Benjamin 47fd2eb7c8 fix(browser): strip PYTHONPATH/PYTHONHOME from browser-use CLI subprocess env
The browser-use CLI runs under its own Python (uv tool / uvx), which
can differ from Hermes's venv interpreter. PYTHONPATH/PYTHONHOME
inherited from the agent process point at Hermes's venv
site-packages, and a child interpreter honors them ahead of its own —
so the CLI imported compiled C-extensions (pydantic_core) built for
the wrong interpreter and crashed with ABI mismatch /
ModuleNotFoundError (issues 83427, 84841, 86006, 86104; hits the
desktop backend on py3.14 and any shell exporting PYTHONPATH).

Strip both vars in _base_subprocess_env() — the CLI manages its own
environment and never needs Hermes's import path.

Salvaged from PR 83471 by Benjamin (@n1majne3), the earliest of two
independent fixes (also PR 84022 by @jklance16, PYTHONPATH-only);
regression test covers both vars and preserves unrelated env.
2026-08-14 16:22:38 -07:00
Teknium d275b96bfd feat(gateway): per-profile MCP server lifecycle RPCs (mcp.servers.*) (#86473)
Adds the full MCP setup surface as profile-scoped gateway RPCs so a
desktop client (Bot Mode's bot editor, the core Capabilities tab) can
add/configure/test/authenticate/remove MCP servers for ANY profile, not
just the launch profile:

- mcp.servers.list (profile) -> configured servers (transport, auth,
  oauth_tokens_present, enabled, tool names; no secret values)
- mcp.servers.add (profile, name, config|preset, bearer_token?) -> reuses
  mcp_config._apply_mcp_preset / _save_mcp_server / _save_bearer_auth_token
- mcp.servers.set_api_key (profile, name, value, env_var?) -> http auth
  header template or stdio env ref, via save_env_value
- mcp.servers.test (profile, name) -> _probe_single_server + oauth state
- mcp.servers.remove (profile, name)
- mcp.servers.oauth.start/poll (profile, name[, session_id]) -> mirrors the
  PROVIDER oauth session/poll model (not the FastAPI dashboard flow): a
  background worker drives the same interactive machinery 'hermes mcp login'
  uses, capturing the browser redirect on a local loopback listener. Client
  opens auth_url via openExternal and polls until status=='approved'.

All handlers are profile-scoped via set_hermes_home_override in try/finally
(mirrors skills.manage). Shared helpers live in tui_gateway/mcp_rpc_helpers.py
and are aliased onto server.py's namespace so the rebound handler bodies
(HandlerRegistry.install) can resolve them — a plain def in methods_tools is
unreachable post-rebind. Reuses hermes_cli/mcp_config.py throughout; no config
logic duplicated; no raw yaml near config.yaml (config-read-guard safe).

Tests: tests/tui_gateway/test_mcp_profile_rpcs.py, 8 E2E against real temp
HERMES_HOME profiles asserting add/list/set_api_key/remove land in the RIGHT
profile's config.yaml and not the launch profile's. 8/8. Registration +
live mcp.servers.list verified in an imported gateway.

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-14 16:17:22 -07:00
Teknium 452465bf78 fix(tests): reap leaked TUI notification pollers between tests
test_run_prompt_submit_requeues_all_unstarted_notifications_with_real_threading
failed twice in one hour on CI slices for two UNRELATED PRs (86371,
86374) with `assert set() == {proc_batch_2, proc_batch_3}`. Root cause:
session.init/create tests earlier in the file start real per-session
notification poller daemon threads and never stop them. Those pollers
outlive their test and keep polling the PROCESS-GLOBAL
process_registry.completion_queue, stealing-and-requeuing the target
test's events mid-assertion so its bounded drain loop can starve.

Reproduced: with 30 leaked foreign-session pollers injected via a
sabotage conftest, the target test fails standalone ~1 in 3 runs with
the exact CI assertion; with the reap fixture active it passed 8/8
under the same sabotage.

Fix:
- tui_gateway/server.py: _start_notification_poller registers
  (stop_event, thread) in module-level _notification_pollers (pruned of
  dead threads on each spawn; threads get a stable
  tui-notif-poller-<sid> name for debugging).
- tests/test_tui_gateway_server.py: autouse fixture sets every
  registered live poller's stop event after each test and joins them
  under ONE shared 3s budget (the poller loop wakes at least every
  0.5s), so no poller survives into the next test. No per-thread
  timeout, no session-dict mutation — a first draft that mutated
  session state and joined per-thread hung the file; full-file runtime
  with this version is 15.2s vs 13.2s baseline.
2026-08-14 16:04:10 -07:00
hermes-seaeye[bot] 8e509c729e fmt(js): npm run fix on merge (#86430)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 22:48:47 +00:00
Teknium 7d96537bc8 fix(desktop): the main agent's model pick persists as the profile default (#86414)
* fix(desktop): the main agent's model pick persists as the profile default

Reported: the default bot switches to the OpenAI API account instead of
the user's subscription, and doesn't retain the previous selection.

Root cause: the composer model picker always sent the switch as
--session scope, even for the PRIMARY profile's main agent. So the pick
never wrote config.yaml model.provider — and with model.provider unset,
resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env
var and picks OpenAI/OpenRouter. The subscription the user selected was
only ever a per-session override that evaporated on the next session.

Fix: when the pick targets the primary profile's main agent
(touchesPrimary), send --global so it persists to config.yaml
(model.default + model.provider) via the existing model-switch persist
path. A SET model.provider already outranks the OPENAI_API_KEY env var
in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the
chosen provider across restarts. Secondary chat tiles stay --session so
picking a model in one chat never rewrites the profile default (the
cross-session-contamination guard the old comment protected).

No change to resolve_provider's priority chain, so #29285 (an explicit
env key beating a STALE oauth login) is untouched — we simply make the
user's explicit main-agent selection the config default it always
should have been.

* MoA presets stay session-scoped; update tests for primary-persist intent

Fix CI (ui shard 3of3): the primary main-agent pick now persists via
--global, but MoA (mixture-of-agents) presets must NOT — a transient
orchestration choice can't become the global gateway default. Exclude
provider==='moa' from the persist path (stays --session). Update the
primary-picker test to assert --global (the new intent) and keep the
MoA + secondary-tile tests asserting --session (the guards that prove
the narrowing). 19/19 green locally.
2026-08-14 15:40:53 -07:00
hermes-seaeye[bot] 31e571acf6 fmt(js): npm run fix on merge (#86407)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 22:16:25 +00:00
Teknium eb20188d2e style: import order + padding lines in terminal.test.ts 2026-08-14 15:10:42 -07:00
Teknium 3f7140fbed chore: contributor email mapping for salvaged PR #66538 2026-08-14 15:10:42 -07:00
Teknium 5d8569d043 fix(tui): gate BSU/ESU on capability for ALL write paths, not just alt-screen
Follow-up to the salvaged #66538 commit: the ZELLIJ env gate fixed
detection, but writeDiffToTerminal still wrapped main-screen frames in
BSU/ESU unconditionally (skipSyncMarkers was only set for alt-screen).
Under Zellij the multiplexer re-chunks the stream, so the markers buy no
atomicity and stale frames leak into main-screen scrollback as the
repeated chrome reported in #66490. The renderer now passes
!SYNC_OUTPUT_SUPPORTED for every write path; supported terminals keep
today's behavior on both screens. Adds emitted-frame regression tests
for both marker modes.
2026-08-14 15:10:42 -07:00
Brian Sweatt d509e6df2f fix(tui): don't trust DEC 2026 synchronized output under Zellij
isSynchronizedOutputSupported() only excluded tmux, so running inside
Zellij under an outer terminal that advertises DEC 2026 (e.g. WezTerm
via TERM_PROGRAM) returned true. Zellij, like tmux, sits between us and
the outer terminal and chunks the stream, breaking BSU/ESU atomicity and
pushing old TUI frames into scrollback as repeated output.

Guard on the ZELLIJ env var (set to the session index, e.g. "0") the
same way we already guard on TMUX. Also thread an optional env argument
through the function so the behavior is unit-testable, mirroring
needsAltScreenResizeScrollbackClear() in the same module.

Closes #66490
2026-08-14 15:10:42 -07:00
Max Hsu 802a60a150 test(gateway): pin restoration of pre-existing HERMES_HOME override
Per hermes-sweeper review suggestion on #50242: the existing tests only
covered the unset case (override is None after the call). Add a test
where a caller already holds an override — _persist_live_session_system_prompt
must build the prompt under the session's profile and then restore the
caller's override via the reset token, not clear it to None.
2026-08-14 15:08:41 -07:00
Max Hsu 308bb56349 fix(gateway): bind profile override in _persist_live_session_system_prompt
Fixes #50233

_persist_live_session_system_prompt rebuilds the system prompt after a
live model switch (/model), but _start_agent_build's finally block has
already reset set_hermes_home_override by then. load_soul_md() and
build_skills_system_prompt() call get_hermes_home() which falls back to
the root ~/.hermes, loading the wrong SOUL.md identity and skills for
the session's profile.

Fix: set_hermes_home_override(session["profile_home"]) before calling
agent._build_system_prompt() in _persist_live_session_system_prompt,
and reset it in a finally block. Also upgrade the failure log from
DEBUG to WARNING so silent profile-wrong-prompt issues are visible.

The first-prompt lazy-build path is unaffected — _run_prompt_submit
already re-sets the override before run_conversation. The /model slash
worker path does not, which is the gap this fixes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 15:08:41 -07:00
Teknium 4d1ef7fbd2 style: satisfy curly + padding-line eslint rules in usage comparator 2026-08-14 15:07:24 -07:00
Teknium 513f683aa7 fix(tui): widen usage comparator to key union + regression tests
Follow-up to the salvaged #41484 commit:
- usageChanged() iterates the union of Usage keys generically instead of a
  hardcoded field list — the original PR's list omitted active_subagents
  (consumed by the status rule's subagent segment and resume hint), which
  would have suppressed legitimate updates
- The memo(StatusRule) half of the original PR is intentionally dropped:
  main's StatusRule gained battery/subagent/resume-hint segments since,
  and the wrapper broke the direct-call test seam. The load-bearing fix is
  the stable usage reference: unchanged deltas no longer mint fresh objects,
  so $uiState subscribers stop re-rendering per streaming event
- Regression tests: unchanged-reference retention, active_subagents-only
  update, key-union asymmetry
2026-08-14 15:07:24 -07:00
liuhao1024 8606867ebc fix(tui): reduce status bar flicker during streaming
The status bar flickers visibly during streaming because every state
patch (thinking.delta, reasoning.delta, tool.*, usage notifications)
creates a new $uiState object, forcing StatusRulePane and StatusRule
to re-render and redo expensive layout calculations on every event.

Three fixes applied:

1. Stabilize usage object references in createGatewayEventHandler.ts
   - Add mergeUsageStable() that shallow-compares Usage fields before
     creating a new object. When values haven't changed, returns the
     existing reference, preventing unnecessary StatusRule re-renders.

2. Memoize expensive computations inside StatusRule (appChrome.tsx)
   - statusBarSegments(cols) → useMemo([cols])
   - modelLabel() → useMemo([model, effort, fast])
   - ctxLabel, bar → useMemo([usage fields, segs])
   - Tail segment budget + fits() calculations → single useMemo block
     covering all progressive-disclosure logic

3. Wrap StatusRule in React.memo (appChrome.tsx)
   - Combined with stable usage references, allows React to skip
     re-renders when props haven't actually changed.

Fixes #41480
2026-08-14 15:07:24 -07:00
David Gutowsky 3abe6cc503 fix(gateway): bind profile HERMES_HOME override in ephemeral agent threads (#50233)
Normal prompt turns bind session['profile_home'] via set_hermes_home_override
before run_conversation, but the two ephemeral RPC paths (prompt.background,
preview.restart) spawn a fresh AIAgent on a new thread where the HERMES_HOME
ContextVar does not propagate — so a background/preview turn under a
non-default profile ran against the wrong home. Re-bind for the duration of
the ephemeral turn and restore in finally, mirroring the normal prompt turn.

Surgically reapplied from PR #50777 (handlers moved to methods_prompt.py
since the PR was authored; handler bodies rebind onto server.py globals, so
the original pattern transplants verbatim). Includes the contributor's
regression tests unchanged.
2026-08-14 15:04:46 -07:00
Teknium 5821691642 test(dashboard): extend ChatPage Terminal fake with onScroll/buffer/scrollToBottom
The resume follow-scroll fix subscribes term.onScroll and reads
term.buffer.active; the ChatPage test fake predates both and crashed
the suite with 'term.onScroll is not a function' (4 unhandled errors).
2026-08-14 15:00:05 -07:00
Teknium 7f7ca89255 fix(dashboard): weave resume follow-scroll into sanitizer write path + contributor mapping
Follow-up to the salvaged #59713 commits: main's ChatPage gained the
PTY resume sanitizer + hydration machinery after the PR was authored, so
the write-callback follow-scroll is applied to the sanitized write path
(term.write(rendered, followScroll)) rather than the raw ev.data writes
the original diff targeted. Also adds the contributor email mapping.
2026-08-14 15:00:05 -07:00
VBS2004 4406d8ca6a test(dashboard): cover resume-scroll stick-to-bottom logic
Extract the resume-scroll decision into lib/pty-scroll (isViewportPinnedToBottom, shouldFollowPtyOutput) and add focused vitest coverage, per review on #59591.
2026-08-14 15:00:05 -07:00
VBS2004 82e05b2040 fix(dashboard): scroll terminal to bottom when resuming session
Fixes #59591
2026-08-14 15:00:05 -07:00
kshitij c925b523a1 test: simulate contended wait for resolve+reload path
Two tests asserted resolve+reload events but acquired the lease
immediately (no wait). After gating the reload behind _lease_waited,
these tests must simulate a contended wait via on_wait(0.0) to
exercise the resolve+reload path.
2026-08-15 03:22:16 +05:30
kshitij d729019e46 fix(sessions): harden turn lease refresher and skip unnecessary reloads
Three follow-up fixes to the cross-process turn lease:

1. Move _clear_durable_turn_lease_interrupt() to after the refresher
   thread join in the outer finally. A refresher firing between the
   inner stop and the join could set an interrupt that survives the
   clear and poisons the next turn on a cached agent.

2. Set self._interrupt_message in the except branch of _interrupt_turn
   so _clear_durable_turn_lease_interrupt can match and clear it even
   when self.interrupt() itself raised.

3. Gate the conversation_history reload behind _lease_waited so an
   immediate acquisition (no contention) does not replace the
   in-memory history and cause an unnecessary prompt cache miss.
2026-08-15 03:22:16 +05:30
fangliquanflq 6b25e67047 fix(sessions): scope refresh interrupts to active turns 2026-08-15 03:22:16 +05:30
fangliquanflq 967391cd4b fix(sessions): stop lease refresher at turn boundary 2026-08-15 03:22:16 +05:30
fangliquanflq 19b1204392 fix(sessions): revive uncontested expired turn leases 2026-08-15 03:22:16 +05:30
fangliquan f1025b2c00 fix(sessions): fence transcript writes with the turn-lease holder
Refresh-loss interrupt is cooperative, so a stalled writer could still flush after another process reclaimed the conversation. Carry the holder into append_message / append_messages_batch and reject the write in the same SQLite transaction when the lease row is missing, expired, or owned by someone else.
2026-08-15 03:22:16 +05:30
fangliquan c21efeeb52 fix(sessions): keep turn lease across inherited-marker compressions
Presence-only _delegate_from/_branched_from checks stopped the lease walk on
continuations that copied a delegate's model_config, so the first refresh
after rotation missed the parent-key lease and hard-interrupted. A failed
get_session probe also skipped acquire entirely. Walk the lineage inside
the write transaction and treat a probe error as contended, not a fresh
session.
2026-08-15 03:22:16 +05:30
fangliquan 5e2be43fd4 fix(sessions): harden cross-process turn lease wait and refresh
Honor interrupts while waiting for admission, stop the turn when refresh
loses the lease, poll once per second under contention, and test dead-PID
reclaim.
2026-08-15 03:22:16 +05:30
fangliquan 3b09456019 fix(sessions): surface wait status for cross-process turn leases
Emit lifecycle notices while waiting on another process, and return a
resend-friendly timeout result instead of a bare TimeoutError.
2026-08-15 03:22:16 +05:30
fangliquan 6e929a9694 fix(sessions): serialize turns across processes 2026-08-15 03:22:16 +05:30
Gille a373a1b749 fix(desktop): tolerate omitted get-windows on ARM64 2026-08-14 16:51:00 -05:00
fangliquanflq 689a4be6f2 fix(desktop): degrade get-windows on Windows ARM64
(cherry picked from commit 6997b5a97e72aa1ffc98533aaa100e84857b1fbe)
2026-08-14 16:51:00 -05:00
webtecnica 58e4bb04d6 fix(desktop): make get-windows optional dep so Linux build doesn't break (#85377)
(cherry picked from commit f25e3467e6a28911067b416bf6647c8c0b3254a6)
2026-08-14 16:51:00 -05:00
Teknium 5269c8f69b chore: contributor email mapping for salvaged PR #50873 2026-08-14 14:50:45 -07:00
QLYF 253d4aa996 fix(desktop): add focus to ZOOM_REASSERT_WINDOW_EVENTS for high-DPI displays
On Windows with high-DPI displays (150%+ scaling), Chromium
re-evaluates zoom on focus change (alt-tab). The persisted zoom
level was only re-asserted on show/restore/resize/move events.

Add 'focus' to zoomReassertWindowEvents() so the zoom level is
restored when the window regains keyboard focus, covering both
main and secondary session windows through the existing
installZoomReassertOnWindowEvents() abstraction.

Extend zoom.test.ts to exercise the registered focus handler.

Fixes #50837
2026-08-14 14:50:45 -07:00
Brooklyn Nicholson b66b4a50cf test(desktop): cover the torn-renderer-bundle detector
renderer-bundle.ts shipped in #85887 as pure/injectable but had no unit
coverage. Lock the contract that heals a torn self-update: the boot loader
must prefer a complete generation and only report a repair when every copy
is torn.

parseModuleAssetRefs: module scripts + modulepreload only (not stylesheets
or classic scripts), CDN/absolute refs dropped, ./ and query/hash stripped.
missingRendererAssets: intact -> [], torn -> the dangling chunk(s),
per-copy dir-relative existence (the split app.asar vs app.asar.unpacked
case), and an unreadable/module-free index is not treated as torn.
2026-08-14 16:48:02 -05:00
hermes-seaeye[bot] e1098cbb18 fmt(js): npm run fix on merge (#86379)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 21:44:50 +00:00
Buff Pesos 56f1afc834 feat(dashboard-auth): extend RFC 8252 native sign-in to password providers (system-browser autofill) (#75808)
* feat(dashboard-auth): extend RFC 8252 native sign-in to password providers

The desktop app runs password sign-in for gated gateways in an embedded
Electron BrowserWindow, where OS password managers (macOS Passwords /
iCloud Keychain autofill) cannot reach the form — Chromium-in-Electron
has no bridge to them, so users retype credentials by hand even though
the /login form already carries the right autocomplete attributes.

The existing RFC 8252 native flow (system browser + loopback + PKCE)
solves exactly this for OAuth providers, but was explicitly disabled for
password providers on the grounds that they have "no IDP round trip to
broker". The brokering is still worth having: it moves the credential
form into the system browser, where password-manager autofill just works.

Gateway-only change; the desktop needs no changes (runNativeLogin is
already page-agnostic), and older desktop builds pick the capability up
automatically once the gateway advertises it:

* /auth/native/authorize now accepts a supports_password provider:
  register the pending broker authorization as usual, then 302 the
  system browser to the interactive /login form with the opaque
  broker_state in the gateway's PKCE cookie (the same server-controlled
  channel the OAuth branch uses) instead of an IDP redirect.
* /auth/password-login: when the server-set PKCE cookie carries a
  broker handle, a successful credential check completes the pending
  authorization exactly like the /auth/callback native branch — mint
  the one-time loopback code, return the loopback redirect (validated
  loopback-only at authorize time) as `next`, clear the PKCE cookie,
  and set NO session cookies. A lapsed broker is a clean 400 telling
  the user to restart sign-in; a failed credential attempt leaves the
  pending entry intact so the user can retype.
* /api/status now advertises "native_pkce" whenever any interactive
  session provider is registered (previously only for non-password
  providers), so the desktop selects the system-browser strategy for
  password-only gateways.

Security posture is unchanged from the existing flow: loopback-literal
redirect_uri enforcement, PKCE S256 binding, single-use short-TTL codes,
constant-time comparison, and the same rate limiter on password attempts.

Tests: full authorize → /login → password-login → loopback → token →
bearer round trip, wrong-password keeps the pending entry, lapsed broker
→ 400, no-broker browser login keeps minting cookies, and the /api/status
advertisement for password-only gateways.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dashboard-auth): bind native password completion to the authorize-time provider

Review follow-ups for #75808:

* /auth/password-login now enforces that body.provider matches the
  provider recorded in the server-set PKCE cookie by
  /auth/native/authorize before completing a pending native
  authorization. /login renders a form for every session provider, so
  without this a native flow started for provider A could be completed
  with provider B's credentials, binding B's session into A's pending
  entry. The mismatch is rejected BEFORE credential verification (no
  session minted, no oracle) and preserves both the pending entry and
  the cookie, so the user can still submit the correct provider's form.
  Covered by a two-password-provider E2E regression test.

* Update the two docs spots that still said password-only providers do
  not advertise native_pkce (website desktop-native-signin guide and the
  auth_flows type comment in web/src/lib/api.ts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: map contributor email for #75808 (buffpesos)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-08-14 21:39:01 +00:00
Teknium 518bc90e74 fix(agent): bound HERMES_HOME override wins over shared session-db home
The messaging gateway multiplexes profiles over ONE shared launch-home
state.db, binding the profile per turn via the HERMES_HOME ContextVar
(copy_context into the worker thread). _agent_home derived the home from
db_path unconditionally, so on that lane the launch home stomped the
correctly-bound profile — deterministically inverting the leak #86313
fixed (found by @kshitijk4poor's post-merge probe; @helix4u flagged the
plugin-metadata half).

- _agent_home: bound override wins; session_db home is the unbound-thread
  fallback
- _plugin_session_info: profile_name derives from _agent_home too
- full-prompt wiring regression (SOUL + skills + profile line on a bare
  thread with the bot's DB) — reverting any call-site wire fails it;
  multiplex, bare-thread, and plugin-metadata cases each pinned;
  sabotage-verified both new tests fail against the merged behavior
- skills LRU cap 8 -> 32 (key is now per-profile x platform)
2026-08-14 14:37:00 -07:00
kshitij 1347e31113 fix(gateway/relay): guard platforms dict access when config is a list
Same bug class as #83185: `hermes gateway setup` writes
`gateway.platforms` as a list, but the relay path called `.get()`
on it without an isinstance guard.
2026-08-15 03:05:23 +05:30
Seashore Shi 5bc13cf574 fix(gateway): tolerate list-shaped gateway.platforms from setup (#83185)
`hermes gateway setup` writes `gateway.platforms` as a LIST of
enabled platform names (e.g. `- telegram`), not a dict.  Treat any
non-dict shape as "no per-platform overrides" instead of crashing
on `.get()` for every incoming turn (#83185).

Co-authored-by: SeashoreShi <seashore.shi@gmail.com>
2026-08-15 03:05:23 +05:30
Brooklyn Nicholson 3d34b1916d feat(desktop): add 'Open in terminal' to the session menu
Sits under 'New window' in the Open group and resumes the chat in the user's
own terminal, starting in the session's workspace and pinned to its profile.
Hidden on a remote connection, where the emulator is local but the session
isn't.
2026-08-14 16:27:01 -05:00
Brooklyn Nicholson 867eb1a381 feat(desktop): expose openSessionInTerminal over the capability bridge
Resolves the same backend the app launches (usually a venv python running
-m hermes_cli.main), writes the launcher script, and spawns the terminal
detached so it outlives the app. Resolution only — never ensureRuntime, which
would start a first-run install from a menu click.
2026-08-14 16:27:01 -05:00
Brooklyn Nicholson 237abc1e9e feat(desktop): resolve the user's terminal emulator and TUI resume argv
Pure helpers for handing a session to an external terminal: the
`--tui --resume <id>` argv, a launcher script that carries the resolved
runtime's command and PYTHONPATH, and per-platform emulator resolution.

macOS opens the .command with no -a so LaunchServices routes it to whichever
app the user bound to shell scripts; Linux leads with Debian's
x-terminal-emulator alternative before the concrete emulators; Windows prefers
Windows Terminal over a cmd console.
2026-08-14 16:27:01 -05:00
Brooklyn Nicholson 4d6f4a6fe7 fix(desktop): local & remote profiles reuse the default socket after 0.20.1
sharedPrimaryRoute() inferred "served by the shared primary backend" from
the mere presence of connection.profile. But pooled backends (a local named
profile, or a per-profile remote override) also carry `profile` so their
WebSocket URL mints against the right backend. Both descriptors looked the
same, so ensureGatewayForProfile() took the shared-primary branch for a
pooled profile and never dialed its socket — Desktop stayed on the default
profile's socket even though the sidebar (REST) listed the right sessions.

Regression from #85665 (d16e236). Tag only the true shared-primary
descriptor with an explicit `sharedPrimary: true` marker in ensureBackend()
and check that marker instead of `profile`. Covers both the local-pool and
remote-override routes — the whole bug class, not one path.

Test asserts both sides of the invariant: a { profile, sharedPrimary: true }
descriptor activates the primary socket without dialing, and a pooled
descriptor carrying { profile } dials its own exact WebSocket URL.

Supersedes #85750, #85778, #85932
Fixes #85777

Co-authored-by: Tigrannnnnnn <122704900+Tigrannnnnnn@users.noreply.github.com>
Co-authored-by: Don Tuttle <11698271+wdon@users.noreply.github.com>
Co-authored-by: plcunha <145560011+plcunha@users.noreply.github.com>
2026-08-14 16:25:16 -05:00
kshitij befc406839 Merge pull request #86358 from kshitijk4poor/chore/author-map-seashoreshi
chore: add SeashoreShi to contributor email map
2026-08-15 02:52:17 +05:30
kshitij 9f004c8217 refactor: hoist binary_extensions import to module level
Lazy import inside _check_binary_document_write was unnecessary —
binary_extensions is a leaf module already imported at line 15.
Hoisted has_opaque_document_extension and is_pdf_path to the existing
module-level import. /simplify-code finding.
2026-08-15 02:51:59 +05:30