Commit Graph

4089 Commits

Author SHA1 Message Date
kshitij 2eaa863112 Merge pull request #93102 from kshitijk4poor/feat/bot-envelope-ttl
feat(bot-mode): envelope TTL + offline fast-fail for bot relay (#93091 item 2)
2026-08-24 01:10:53 +05:30
kshitij 2df849b57a Merge pull request #93103 from kshitijk4poor/feat/bot-attention-badge
feat(desktop): needs-attention badge for background bot failures (#93091 item 3)
2026-08-24 01:08:56 +05:30
kshitij e00d6c1995 fix(desktop): don't push a live connection as absent when its profile fetch blips
Review follow-up: relayAgentsOn() returned [] on ANY error, so a transient
profiles.list timeout pushed a fresh union roster missing a LIVE machine's
agents — and the gateway-side _target_liveness reads 'absent from a fresh
roster' as definitively offline, refusing enqueues with a false
runtime_offline during the ~60s window. Failure now returns null (distinct
from a genuinely empty list); syncRelayRosters reuses the last good rows
for that connection and prunes the cache when a connection truly leaves
profileRoutes. Source-contract test pins null-on-failure + cache fallback.
2026-08-24 01:07:33 +05:30
kshitij 387698a60d fix(desktop): badge active-gateway bots too — resolve the relay attention key for local rows
Review follow-up: the relay drain records attention under
'<connectionId>::<profile>', but local/unannotated roster rows carry no
bot.connectionId — botRosterKey gives 'legacy::name' and botSelectionKey
bare 'name', so a failed relay DM to a bot on the ACTIVE connection never
rendered its badge. BotRow now also checks
'<bot.connectionId || activeConnectionId>::<name>', covering exactly the
rows the user is most likely looking at. Test pins all three lookup shapes.
2026-08-24 00:35:39 +05:30
kshitij 0159b51f2b Merge pull request #93125 from kshitijk4poor/test/93080-flipback-assert
test(desktop): assert the isDisabled flip-back actually notifies
2026-08-24 00:09:52 +05:30
hermes-seaeye[bot] fc0b2a0ab3 fmt(js): npm run fix on merge (#93123)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 18:38:31 +00:00
kshitij 49f3e67e45 test(desktop): assert the isDisabled flip-back actually notifies
Post-merge review follow-up for #93080: the isDisabled guard test
documented 'flipping back also notifies' but never asserted it — a
regression making the true->false transition silent (e.g. gating
disabledChanged on truthiness) would still pass. Pin the flip-back
notify with a beforeFlipBack capture (mutation-checked: gating the
seed on newDisabled truthiness now fails this test).
2026-08-24 00:07:19 +05:30
kshitij 90803f22a5 fix(desktop): track isDisabled flips in the adapter no-op notify gate
Review follow-up: __internal_setAdapter assigned this.isDisabled before
the fast path but never fed it into the new 'changed' flag, so an
isDisabled-only flip on an otherwise-identical adapter swap would have
been silently swallowed. Seed 'changed' with the isDisabled comparison
and add a guard test (mutation-checked: reverting the seed fails it).
2026-08-24 00:04:10 +05:30
kshitijk4poor 4019518edd fix(desktop): stop no-op adapter swaps notifying the thread runtime + derive backend venv from the selected interpreter
Two independent desktop-boot/runtime bugs found driving the app over CDP
against current main, each pinned by a regression test:

1) Adapter no-op notify loop: IncrementalExternalStoreThreadRuntimeCore.
   __internal_setAdapter's fast path (same isRunning + same messageRepository)
   called _notifySubscribers() unconditionally. ChatRuntimeBoundary passes a
   fresh adapter literal every render, so any subscriber whose notification
   re-renders the boundary loops render->setAdapter->notify->render until
   React kills the tile with 'Maximum update depth exceeded' (reproduced live
   on every bot-profile switch; session tile dies behind its error boundary).
   Now the fast path notifies only when extras/suggestions/capabilities
   actually changed.

2) Dual-venv interpreter mismatch: findPythonForRoot() prefers .venv over
   venv, but createPythonBackend() hardcoded venvRoot=root/venv for
   PYTHONPATH. A checkout with BOTH venvs (dev .venv 3.12 + install venv
   3.11) got a 3.12 interpreter with 3.11-compiled native wheels on
   PYTHONPATH and died on the first import (pydantic_core) before the
   gateway bound - the renderer then showed 'Gateway offline' on every
   profile. venvRootForPython() now maps the selected interpreter back to
   ITS venv; root/venv remains the fallback for system pythons only.
2026-08-24 00:04:10 +05:30
kshitijk4poor d4f426792b feat(desktop): needs-attention badge for background bot failures (#93091 item 3) 2026-08-23 23:27:34 +05:30
Teknium 9452bca388 fix(desktop): route bot-chat RPCs to the bot's own gateway via tile ownerRoute (#92956)
The real fix for Bot Mode 'session not found' / endless hang: dispatch
session-scoped RPCs on the OWNING profile's local gateway, using the route
the chat tile already carries — the same multi-connection machinery Sessions
mode uses, which has never had this problem.

Root cause chain:
- A bot chat is a persisted tile that records its exact owner (connectionId +
  profile) in tile.ownerRoute; requestForSessionProfile already dispatches on
  any (connectionId, profile) via the per-profile local gateway pool.
- But wiring's requestGateway resolved the owner via rememberedSessionProfile,
  a $sessions row lookup. Canonical Bot Chats are born hidden (never listed),
  so the lookup missed and fell back to the ACTIVE profile -> prompt.submit hit
  the launch backend that never owned the session -> 4001, and the resume
  ladder re-resolved through the same blind spot, so it hung.
- It also keyed off $selectedStoredSessionId, but a bot chat renders in a TILE
  whose id is $focusedStoredSessionId (selected stays the primary pane), so
  even the row path was reading the wrong session.

Fix:
- wiring requestGateway: resolve owner from the FOCUSED stored id, preferring
  the tile's persisted ownerRoute; fall back to the list-derived profile only
  when no tile route exists. One resolver, every session RPC (submit, resume,
  attach, interrupt, compress) inherits it.
- sdk openSession: synthesize a local ownerRoute from  for bot opens
  that carry no explicit cross-connection route, so LOCAL bot tiles carry their
  owner too (previously only remote routes did). Strictly routing metadata:
  the dial path, all-profiles view, and the route-registry retry check all
  still key off the EXPLICIT route, so a plain local open behaves exactly as
  before (no registry-secondary dial, no forced all-profiles view).

Fixes already-open chats (tile route is persisted, needs no fresh open) and
survives relaunch. 3 tests for sessionTileOwnerRoute. tsc 0 errors.

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-23 06:32:51 -07:00
Teknium d5281f5981 feat(bot-mode): a reclaimed bot chat re-resumes itself — no stale-id error on the next send
When the gateway reaps the runtime behind the open bot chat (idle TTL,
LRU cap, or the WS-orphan mass reap that killed every background bot's
handle at once in the Aug 23 incident), the plugin now hears
session.reclaimed and re-resumes the canonical chat immediately, instead
of leaving the dead handle for the user's next send to trip over.

Matched on the stored id against both claim identities; guarded by the
open generation so a user action mid-re-resume wins; a failed re-resume
is swallowed — the next-send recovery ladder (#92928) stays the
backstop. Feature-detected on host.onEvent; disposed with the other
listeners.
2026-08-23 06:01:10 -07:00
Teknium f530cd2b54 fix(bot-mode): group rooms name renamed bots — 'Lucy is thinking…', never a stale 'Hermes'
groupSpeakerLabel resolved friendly identity for exactly one case: the
literal profile name 'default' → 'Hermes'. A renamed default (core
display_name via 'hermes profile rename', e.g. Lucy) or a Bot Mode title
never reached the room's working line, activity feed, or transcript
speaker prefix — the community report was Lucy's group turns still
reading 'hermes thinking'.

The label now walks the same rungs as displayName(): Bot Mode title
first, then the ACTIVE gateway roster row's display_name (remote/thin
rows are skipped so another connection's default can't lend its name),
then the existing default→Hermes fallback.

Validation: group-chat.test.mjs 87/87, full hermes-bots suite 474/474.
2026-08-23 05:45:24 -07:00
Teknium 2c244f0751 fix(desktop): route hidden Bot Chat RPCs to the owning profile backend (4001 session-not-found) (#92928)
* fix(desktop): route hidden Bot Chat RPCs to the owning profile backend

Bot Mode chats failed with 4001 'session not found' (then hung on retry)
for every bot except the launch profile. Root cause is an identity gap in
the session-RPC router for HIDDEN sessions:

- wiring's requestGateway resolves the owning profile via
  rememberedSessionProfile($sessions, selectedStoredSessionId, active).
- Canonical Bot Chats are born hidden (hermes-agent#86797), so the sidebar
  aggregator NEVER lists them; whenever the in-memory row is absent the
  lookup misses and the resolver silently falls back to the ACTIVE profile.
- prompt.submit then lands on the launch backend, which never owned the
  session -> 4001. withSessionNotFoundResume's session.resume ALSO resolves
  the profile through the same blind spot, so the recovery re-registers the
  wrong backend too and the ladder dies without ever reaching the bot's own
  (healthy) gateway. Log fingerprint: the bot backend shows ws accepts with
  messages=0 and no 'tui prompt accepted' after the reap, while the launch
  backend answers 4001.

Fix at the resolver, so every session-scoped RPC (submit, resume, attach,
interrupt, compress) gets the same answer:

- rememberedSessionProfile: when no session row matches, consult the
  session-owner hint (targetProfile over profile) before falling back to
  the active profile.
- sdk openSession: record an owner hint for LOCAL plugin opens that carry
  an explicit profile (remote routes already did via ownerRoute). Hidden
  sessions have no sidebar row, so this hint is the only durable owner
  record the router can consult.

3 focused tests: hint fallback for a hidden session, targetProfile
preference, and row-over-hint precedence. tsc 0 errors (baseline-equal).

* test: add activeGatewayConnectionId to the full-replacement gateway mock

sdk/index.ts now imports it for the local-open owner hint; the full
vi.mock('@/store/gateway') in profile-routing.test.ts must export every
symbol the module under test imports or all 18 suite tests fail at load.

---------

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-23 05:44:26 -07:00
Teknium bdf10471b5 fix(desktop): Send Diagnostics dialog no longer clips the link behind a horizontal scrollbar
The dialog body's grid used the implicit column, which sizes its track to
unbreakable content: the nowrap view-link <code> forced the track wider than
the dialog, so the description and URL were clipped and the Copy button sat
past the right edge behind a horizontal scrollbar. Both DialogContent body
boxes now pin the column to minmax(0,1fr) so children truncate instead of
widening the track — this hardens every dialog against long unbreakable
content, not just this one.

The view link is also now a real anchor (system browser on click, link
context menu on right-click) inside a data-selectable-text row so the URL
can be highlighted and copied by hand; truncation clips the paint only,
selection still carries the full URL.
2026-08-23 04:59:50 -07:00
Teknium a4c6c6bddb fix(bot-mode): first click on a bot opens its chat — the home no longer bounces over it
The Bots home landing appeared on EVERY first click of a bot whose
canonical Bot Chat had been compressed; only a second click got through.

openRosterBot claimed the center with the durable registry id, but the
session-focus edge fired by the open itself reports the compression-
lineage TIP. releaseStaleOpenBotChat compared tip !== registry id,
declared the claim stale, released it, and the home reasserted over the
freshly opened chat. The second click worked only because the tip was
already focused — no new focus edge fired to sabotage it.

openBotCanonicalChat now returns both identities (registryId + openedId);
the claim carries both; a focus edge matching EITHER keeps it. Foreign
sessions still release, and the legacy no-id draft claim is unchanged.
2026-08-23 04:57:46 -07:00
Teknium 102a30f6fb fix(desktop): typing /voice points at the composer voice button
/voice arms SERVER-side capture (voice.record → PortAudio on the backend
host) — meaningless on desktop, which has its own composer-native voice
conversation (mic menu / Ctrl+B). It was already suppressed from the
slash palette, but typing it got the generic 'advanced' shrug that never
mentioned the button exists. New composer-voice unavailability reason
with a message naming the actual surface.
2026-08-23 04:44:27 -07:00
hermes-seaeye[bot] 891ec3fb6c fmt(js): npm run fix on merge (#92896)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 11:25:25 +00:00
Teknium 2ec229ec5a fixup: roster query keeps SDK ambient owner route; alias index refresh preserved 2026-08-23 04:19:55 -07:00
David Dudok de Wit 7ead9b93de fix(desktop): resolve Bot tiles on their owner 2026-08-23 04:19:55 -07:00
David Dudok de Wit 8523819fcf refactor(desktop): consume upstream Bot owner routing 2026-08-23 04:19:55 -07:00
David Dudok de Wit 9ff7f3325c fix(desktop): preserve cross-realm Bot registry errors 2026-08-23 04:19:55 -07:00
David Dudok de Wit a81854a2bd fix(desktop): preserve Bot tabs across owner lifecycles 2026-08-23 04:19:55 -07:00
David Dudok de Wit 856fc66d19 fix(desktop): bound Bot owner wake races 2026-08-23 04:19:55 -07:00
David Dudok de Wit b42d8279ed feat(desktop): retain Bot group drafts by room 2026-08-23 04:19:55 -07:00
David Dudok de Wit 9b36c2d43c feat(desktop): make new Bot tabs owner-aware 2026-08-23 04:19:55 -07:00
David Dudok de Wit b8b6f43280 feat(desktop): define workspace-scoped pane ownership 2026-08-23 04:19:55 -07:00
David Dudok de Wit 613244cbb1 feat(desktop): organize the global bot roster 2026-08-23 04:19:55 -07:00
Teknium 1ad4733343 feat(desktop): configure per-profile remote overrides from the profile rail
The Electron main has routed a profile with a `profiles.<name>` remote
entry in connection.json to its own pooled backend since
profileRemoteOverride() landed — but the only way to WRITE that entry was
hand-editing connection.json (#91349, design intent from #90223 /
6170f844: this belongs on the profile rail, not the machine-level
Gateways page).

- New "Connect to a remote host…" action on the profile-rail square's
  context menu, opening a URL + token dialog that writes the exact
  `profiles.<name>` shape through the existing typed
  getConnectionConfig/applyConnectionConfig bridge (renderer never
  touches connection.json; tokens ride the existing safeStorage
  encryption path, with the allowPlainTextToken opt-in surfaced on
  keyring-less machines).
- First-time connect shows a one-time confirmation with a plain-language
  risk note; editing an existing override (token rotation) skips it.
- Overridden profile squares carry a "remote" globe badge, and the
  tooltip/aria label names the host. A "Remove remote connection" button
  clears the override (mode: local via the existing coerce path).
- Registry name collision: the dialog warns when the profile name
  matches a v2 connections-registry id/label.
- Token rotation: when switching to an overridden profile fails with an
  auth-shaped error (401/forbidden/invalid token), a re-enter-token
  toast opens the same dialog instead of leaving a silently dead
  profile. Connectivity failures stay generic.
- All five locales updated.

Closes #91349

Design-intent analysis credit: @otfnfn
2026-08-23 03:56:42 -07:00
Teknium 3638961da8 fix(desktop): Bot Mode keeps Cloud alias identity after hosted handoff
A Desktop per-profile alias (e.g. moxie with a Cloud override) routes to a
remote backend's root profile: route { connectionId, profile: 'moxie',
targetProfile: 'default' }. Once the hosted backend answers the roster
itself, the row's identity is (connection, 'default') — a different key
than the alias meta — so the friendly name regressed to the raw Cloud
hostname after activation, and Cloud-only rosters showed generic 'Hermes'
instead of the configured alias (#89131).

Add a connection-exact alias index built from the credential-free route
inventory, keyed by (connectionId, targetProfile). displayName,
botRosterMeta, and botFriendlyNames consult it so the claimed backend row
reads as the alias (and its title/meta), while:

- same-named defaults on OTHER connections never borrow the identity
- two aliases claiming one backend row fail closed
- the local default and un-aliased remote defaults keep existing behavior

Evidence: @TheAirick's controlled candidate testing on #89131.
2026-08-23 03:56:33 -07:00
Teknium c012a364eb fix(desktop): spoken replies use the connected gateway's TTS, not a stowaway local backend
The speak-stream WebSocket resolved its URL through the bare v1
getConnection/getGatewayWsUrl pair, which answers for the PRIMARY backend.
When a registry remote connection rides over a machine that also has a
local Hermes install (the common case — the installer always installs the
full agent), spoken replies dialed the LOCAL backend and hit its
unconfigured TTS, while chat (connectionScoped REST) correctly went remote.
Users saw 'configure STT/TTS' although their remote gateway had voice
fully configured.

Resolve the PCM socket through the same (connectionId, profile) bridges
store/gateway's openSecondary uses, and never overwrite a backend-namespace
profile the registry mint already wrote into the URL (SSH remoteProfile
aliasing, sharedRemote scoping).

Every REST audio call already carried connectionScoped(); this was the one
remaining self-built audio URL. Contract pinned by
voice-playback.routing.test.ts (sabotage-verified: 2/4 fail on the old
resolver).
2026-08-23 03:54:52 -07:00
Teknium 10f0d2278b feat(desktop): client-direct voice — use the active profile's STT/TTS keys from the desktop, no audio relay
Lowest-hop voice path in both directions for desktop + remote gateway:
mic audio goes straight to the profile's STT provider and reply text is
synthesized on the desktop with the profile's TTS provider. The
desktop-gateway link carries only text (which the chat stream carries
anyway). No second key store: GET /api/audio/voice-config returns the
profile's resolved provider/model/language/key using the exact resolution
chains transcription_tools/tts_tool use, over the authenticated REST
channel. Keys live in renderer memory only.

Backend:
- tools/voice_client_config.py: single resolver; per-provider client
  wire shapes (openai-multipart, xai-stt, elevenlabs-stt, openai-speech,
  elevenlabs-tts). Server-host-only providers (local whisper, edge,
  command/plugin) and missing credentials resolve to {mode: relay}.
  xAI OAuth stays relay (bearer refreshes server-side).
- web_server.py: GET /api/audio/voice-config, profile-scoped via the
  same _config_profile_scope seam as /api/audio/transcribe.
- config_defaults.py: voice.client_direct gate (default true).

Desktop:
- lib/voice-client-direct.ts: config fetch keyed by (connection,
  profile) with 60s TTL, provider-direct STT + TTS calls, sentence
  cutter mirroring the server pipeline's contract.
- Dictation (use-prompt-actions + session-tile) tries client-direct
  first; null -> existing relay unchanged; provider rejections surface.
- voice-playback.ts: client-direct speech session as the top rung of
  startSpeechStream/playSpeechText; WS relay + POST fallback unchanged
  below it. Barge-in via the same stopVoicePlayback sequence bump.

Validation: 13/13 backend E2E (real temp HERMES_HOME + real resolution),
live FastAPI TestClient E2E (direct + gate-flip), 15/15 client tests
(wire shapes, scope-keyed caching, rejection surfacing, sentence cutter),
sibling suites 72/72 + 36/36, tsc + eslint + ruff clean.

Docs: voice-mode.md client-direct section ships in this PR.
2026-08-23 03:54:52 -07:00
Teknium d3e087fd8c feat(bot-mode): bots on every Desktop connection can message each other
Connections ARE the peer set: every gateway connected to the Desktop
(local, remote URL, SSH, Hermes Cloud, docker) is now message_agent-
reachable. The Desktop relays over the persistent sockets it already
holds — roster sync per connection, envelope drain/deliver/reply loops —
so cross-connection DMs work exactly like local ones, replies included.

Also fixes the legacy-SOUL gate bug: profiles whose SOUL.md carries the
old plugin-appended protocol silently lost the message_agent tool
because the injection/execution gates keyed on protocol-section
non-emptiness instead of managed-install.
2026-08-23 02:16:11 -07:00
Teknium 0430e3c719 fix: harden the metadata-walk probe (directory-named plugin.js regression test, lint)
Follow-up on the #91828 salvage: regression test for the subtle
resolveDiskPluginEntry branch that rejects a DIRECTORY literally named
plugin.js, and lint fix. The truncation-fix semantics from #92809 are
preserved: an oversize source keeps its error inventory row (returns
true — the file exists and was fully probed), while a vanished file
returns false so the scanner reconciles the ghost.
2026-08-23 02:14:20 -07:00
Axl Ibiza 6a4e212ee0 fix(desktop): stop missing plugin entries from flooding IPC logs 2026-08-23 02:14:20 -07:00
hermes-seaeye[bot] f377140e3d fmt(js): npm run fix on merge (#92815)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 08:45:37 +00:00
Teknium ecf63ad3f3 fix: desktop plugins over 512 KiB no longer load truncated
Runtime desktop plugins read their source through hermes:readFileText,
the preview IPC that silently truncates at TEXT_PREVIEW_MAX_BYTES
(512 KiB) — a larger plugin.js evaluated as a partial file (cryptic
syntax error, or worse, a half-module that parses).

- electron/main.ts: dedicated hermes:readPluginSource handler — full
  read, 16 MiB cap enforced as a hard EFBIG via resolveReadableFileForIpc
  (same path hardening + sensitive-file blocking), never truncation.
- preload.ts / global.d.ts: bridge + types (optional — older shells).
- runtime-loader.ts: loadDiskPlugin reads via readPluginSource; on older
  shells falls back to readFileText but FAILS LOUDLY on truncated:true
  (error toast + error inventory row) instead of evaluating a partial
  file. Existence probe keeps the preview read (metadata is enough).
- tests: full-read path, loud old-shell truncation failure (sabotage-
  verified), small-plugin fallback.
2026-08-23 01:39:50 -07:00
Teknium 8804e78354 feat(dashboard): Desktop and dashboard read the update receipt instead of inferring success (#91277 Phase-1 bullet 3)
Builds on @mrsucesso's durable-marker recovery (previous commit):

- GET /api/hermes/update/receipt — the full durable receipt (steps,
  skips, gateway restart outcome, fleet matrix) + compact summary; the
  authoritative update-outcome record (written by every run since
  #91283, including refused/failed).
- /api/actions/hermes-update/status now attaches the receipt summary,
  and when BOTH the in-memory registries and the update.log marker are
  gone (dashboard restarted + log rotated — the #81193 state), a
  finished receipt reports the outcome: success→0, partial→1. A
  still-running receipt proves nothing (clients keep polling).
- Desktop (updates.ts): the apply poll reads the attached receipt — a
  finished receipt whose run started at/after this apply is
  authoritative, replacing timeout-based failure inference across the
  update's restart gap ('Backend update failed' on successful updates,
  #81193; 'boot failed' during update restarts, #87359).

Live-verified: real uvicorn server + real UpdateReceipt writer (the
exact code hermes update runs) over real HTTP — receipt endpoint 200
with summary; #81193 state (no registries, no marker) reports success
from the receipt alone; partial receipt with a DOWN fleet row maps to
exit 1 (no false success).
2026-08-23 00:50:41 -07:00
Teknium e6962b818c style: eslint --fix across src/ + electron/ (CI lints the full tree); drop unused destructure 2026-08-22 22:35:55 -07:00
Teknium 894d02191d Merge current main 2026-08-22 22:26:14 -07:00
Teknium 4e4ee7ee75 style: eslint --fix on merged TS surfaces 2026-08-22 22:25:59 -07:00
Teknium 0404020f7b Merge PR #90006: connection-bound Bot Mode actions, reconciled with name-identity + fail-closed canonical resolution
Salvage of saralilyb's remote-bot routing work onto current main:
- kept: immutable (connectionId, profile) owner capture, requestForBot
  routing, backendTargetProfile aliasing, group session owners,
  connection-qualified deletion, focused-owner atoms, remote roster
  merge, Electron profile-delete routing, sdk/store/transcript changes
- reconciled: canonical Bot Chat resolution stays NAME-identity (the
  'Bot Chat' registry row) and FAIL-CLOSED on lookup errors — now
  consulted on the bot's own source via the captured owner route, so
  remote bots get the same no-fork guarantees
- dropped: pointer-pin plumbing (preferredSessionIds, saveBotMeta chat
  writes, pin verification) — superseded by name-identity on main;
  renderer-side remote DM delivery (deliverRemoteRosterMentions /
  pollRemoteDmReply / ensureRemoteCanonicalChat) — superseded by the
  message_agent tool architecture (#91802/#91915: middleware identifies,
  never delivers); pointer-era test files deleted on main
- openStoredBotChat/createCanonicalChat: remote opens keep Desktop's
  chrome home (keepAllProfilesScope: true on routed opens); local bots
  keep the measured workspace re-home
- prepareBotSource: capability gate only — routed RPCs never require
  activation authority
2026-08-22 22:24:54 -07:00
hermes-seaeye[bot] 8b09a9df84 fmt(js): npm run fix on merge (#92718)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 05:11:16 +00:00
hermes-seaeye[bot] d49d495c2b fmt(js): npm run fix on merge (#92714)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 05:06:17 +00:00
Andrex Ibiza, MBA 38ce2d7553 fix(desktop): enforce exact route identity authority
Make explicit registry qualification authoritative: only a current exact ID is accepted, while blank, malformed, unknown, or retired claims fail closed without endpoint inference. Restrict genuinely unqualified legacy descriptors to the shared full-envelope URL/Cloud/SSH matcher, reject zero or multiple matches, normalize SSH host/user identity, and prove remote-primary restoration keeps the exact (connectionId, profile) tuple.

Closes #90048.

Prior work by @teknium1 in #89719 and #88922, @andrexibiza in #90913, and @AndreasG78 in https://github.com/NousResearch/hermes-agent/issues/90048#issuecomment-5375227679 shaped this implementation. @saralilyb's #90006 remains downstream consumer context; the production stopgap is credited but excluded because registry primary does not prove route ownership.
2026-08-22 22:01:39 -07:00
Teknium c942cd9ea1 fix(desktop): settings scope requests can never target primary by accident
The settings 'Applies to' store uses null = 'follow the active profile',
but the API helpers (profileScoped/capabilityScoped) use null = 'target
the primary/default backend'. Every page that passed the raw override to
a request silently read/wrote the primary profile whenever no override
was set — writes landed on the right profile via other paths while reads
repainted primary's values, so profile model changes appeared to revert
(#90549 class).

Close the class at the seam instead of per call site:
- store/settings-scope: new $settingsRequestProfile computed — the
  request-shaped scope (string | undefined, never null). Documented as
  THE value to hand to API helpers.
- config-settings, keys-settings, messaging: consume the request-shaped
  computed; ModelSettings/MemoryConnect/ProviderConfigPanel/
  useEnvCredentials props narrowed to string | undefined so a
  primary-targeting null can no longer be plumbed through.
- keys-settings site was a live third instance: getEnvVars(null) read
  primary's env store on non-default profiles.

Regression tests: store computed shape, ModelSettings unscoped+scoped
reads, KeysSettings unscoped fetch (all fail against the old behavior;
sabotage-verified).
2026-08-22 21:55:39 -07:00
Carl Taylor abd7f75b8d fix(desktop): keep Messaging on active profile 2026-08-22 21:55:39 -07:00
Klaus Suppan 680b11503c fix(desktop): model settings follow active profile instead of primary
ModelSettings passed scopeProfile (null when following the active
profile) directly to the Hermes API helpers. The helpers interpret
null as "target the primary/default backend", not as "follow the
active profile". This meant that when a non-default profile was
active (e.g. local with LM Studio), the model picker showed the
primary/default profile's providers instead — LM Studio was
invisible.

Fix: convert null → undefined before calling the API helpers, so
profileScoped() falls back to the app-wide active profile.

Added regression test verifying the helpers receive undefined (not
null) when no scope override is set.
2026-08-22 21:55:39 -07:00
Teknium 87b645f52c fix(desktop): a failed Bot Chat registry lookup no longer forks the bot's forever chat
findExistingCanonicalChat() swallowed every lookup error and returned
null — indistinguishable from 'this bot has no Bot Chat yet' — so a
transient RPC failure against a just-restarted backend (the exact
post-desktop-update window) sent createCanonicalChat() straight to
session.create, minting a fresh 'Bot Chat' while the real one (data
intact, hidden) still held the canonical title. Users experienced this
as bots losing all context after every desktop update.

The lookup now fails CLOSED: a failed registry consultation throws,
both open paths surface their existing 'try again' toast, and
session.create can never fire off an unknown ownership state.

Tests: two new VM-executed regression tests (sabotage-verified — both
fail with the old fail-open catch); hide-bot-chats source-shape regex
updated for the new layout. 364/364 plugin tests green.
2026-08-22 21:24:30 -07:00
hermes-seaeye[bot] 4b860d8193 fmt(js): npm run fix on merge (#92399)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-22 17:34:58 +00:00