Commit Graph

23518 Commits

Author SHA1 Message Date
Teknium 9aa1413781 feat(desktop): extend kanban native notifications to blocker/failure events
Builds on @nductien's completion-notify module (PR #87705):

- Notify on the gateway watcher's full terminal set — blocked, gave_up,
  crashed, timed_out, block_loop_detected — not just completed. A worker
  hitting a blocker while the user is away was the original community ask.
- Route all notification copy through the kanban plugin i18n bundles
  (en/ja/zh/zh-hant), with an English-bundle fallback when the translator
  isn't bound yet.
- Wire the ctx.os.notify door so events also fire a NATIVE OS notification
  while the user is away from the Hermes window (host.notify toast covers
  the foreground). OS-door failures are isolated from the toast path.
- Docs: Desktop notifications section in kanban.md, including the
  app-running coverage window.
2026-08-17 17:00:00 -07:00
nductien 1802620847 chore(desktop): remove local markers from kanban notify comments 2026-08-17 17:00:00 -07:00
nductien a30636f817 feat(desktop): surface native OS notification on Kanban task completion
When a background agent completes a Kanban task, the Desktop app now surfaces a native OS notification with the task summary and a one-click "Open Kanban" action.

Rides the existing /events WebSocket (onEventsFrame in api.ts). A
new module completion-notify.ts implements cursor-based per-board
deduplication with baseline from GET /board, fail-closed on unknown
baseline, and reconnection safety. The notification uses the existing
host.notify() and host.navigate() SDK APIs.

Files changed:
- apps/desktop/src/plugins/kanban/api.ts (+8, -1) — wire completion-notify
- apps/desktop/src/plugins/kanban/completion-notify.ts (+89, new)
- apps/desktop/src/plugins/kanban/completion-notify.test.ts (+378, new)

19 targeted tests pass (0 fail).
2026-08-17 17:00:00 -07:00
Teknium 7999ac6e3d feat(desktop): shared 'Applies to' profile scope across settings pages and Messaging
Bring the per-profile scoping affordance the Gateway page already has to
every config-backed settings surface. A new shared nanostore
($settingsScopeOverride in store/settings-scope.ts) holds one "Applies to"
selection that persists across pages; a reusable SettingsProfileScope chip
row (app/settings/profile-scope.tsx) renders it. Hidden with fewer than two
profiles, and null (follow the active profile) keeps every request on the
exact pre-existing unscoped path.

Scoped pages:
- Model / Workspace / Safety / Memory & Context / Voice / Chat / Advanced
  (ConfigSettings): config record + schema queries and the debounced
  autosave PUT now carry the selected profile; the inner page remounts per
  scope so drafts can't leak across profiles. ModelSettings threads the
  scope through model info/options/aux/MoA fetches and assignments; memory
  provider config/OAuth panels follow.
- Tools & Keys (KeysSettings via useEnvCredentials): env list + save/
  reveal/delete target the selected profile's store.
- Messaging overlay: platforms, enable/save/clear, pairing approve/revoke
  are scoped; the list blanks on scope switch so stale rows can't be
  toggled against the wrong backend.

Plumbing: hermes.ts REST helpers gain optional trailing `profile`
parameters (config, schema, env, memory provider, messaging, pairing,
model endpoints) resolved through the existing profileScoped() ladder —
omitting them is byte-identical to before. The cron model-impact warning
is skipped for scoped applies (it belongs to the other profile's backend).
An app-wide profile switch drops the override so edits can't silently
stay pointed at the previous target.

i18n: new settings.profileScope.{appliesTo,editsProfile} keys in
en/zh/zh-hant/ja/ar.

Capabilities (app/skills) and Scheduled Jobs (app/cron) verified: both
already thread their profile scope through every fetch and mutation.
2026-08-17 16:55:14 -07:00
Teknium 56de3c1428 fix(cli): persist one-shot resumed-session turns (Bot Chat bot-to-bot messages)
Bot Mode's bot-to-bot send (`hermes -p <bot> chat --in ~ -c "Bot Chat"
--create-if-missing -Q -q "..."`) runs one turn and exits. When the turn's
in-loop transcript flush failed transiently (state.db write-lock contention
with a multiplex gateway), the one-shot path had no end-of-run durable
retry: the reply reached stdout and agent.log while the resumed titled
session's stored history never changed (#88583). The interactive CLI is
immune — it retries the flush on the next persist point and finalizes the
row on quit — but every one-shot exit path lacked both.

Fix the whole class with cli._flush_one_shot_session_store():

- final _persist_session retry at one-shot exit (idempotent — per-message
  persisted-marker stamps mean already-written turns are not re-written)
- drain queued async token-accounting deltas
- end_session(..., "cli_close") so resumed/created titled session rows no
  longer dangle open forever after one-shot runs

Wired into _finalize_single_query (quiet -Q -q AND human -q paths, ahead
of memory-provider shutdown so nothing later can lose the turn) and into
the kanban SIGTERM handler before os._exit(0), which skips atexit and the
SessionDB token-drain hook entirely (same gap class as PR #50881).

Handed-off sessions (#88234) and persistence-isolated forks
(_persist_disabled) are skipped.

Fixes #88583

🤖 Generated with Hermes Agent
2026-08-17 16:55:07 -07:00
Teknium 54b0a5bc46 chore: map contributor emails for @loafoe and @k0rnacki 2026-08-17 16:54:01 -07:00
k0rnacki dd6e098986 test(api_server): pin the recovery-net alias guards
Three cases in TestCreateAgentModelRecovery: the alias is never
cached, a poisoned cache is never served, and legitimate
last-known-good recovery still works.
2026-08-17 16:54:01 -07:00
k0rnacki 1352439036 fix(api_server): recovery net must never recover the advertised virtual alias
The #35314 empty-dispatch recovery cache accepts any previously
resolved model string, including the advertised virtual model
(hermes-agent by default) — which is never a dispatchable model.
Guard both directions: never store the alias in _last_resolved_model,
and never serve a cached value equal to it. The alias now survives at
most one turn and cannot propagate across turns through the cache.

Class bug behind #79101; complements the session-row symptom fixes
(#72739, #79102).
2026-08-17 16:54:01 -07:00
Andy Lo-A-Foe 152b1940d7 fix(api-server): stop persisting the virtual model alias as a session's model
Salvaged from #72739 (net diff onto current main; the read-side legacy-row
guard now flows through the shared _stored_session_model() helper that
landed in #88751, keeping one resolver for both chat sites).

POST /api/sessions persisted the advertised virtual alias (hermes-agent)
whenever the request omitted model or echoed the alias back; later turns
replayed it upstream as a real model id and every turn on the session
failed. Null the alias in _session_runtime_request_from_body() — shared by
session create, chat, chat-stream, and model-lock — and stop the
create-handler's raw-body fallback from bypassing that normalization.
2026-08-17 16:54:01 -07:00
hermes-seaeye[bot] 77d6c78cf5 fmt(js): npm run fix on merge (#88760)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-17 23:46:08 +00:00
Teknium ae5c4c1672 feat(desktop): Discord-style group-chat rows in Bot Mode, opening in the main chat window
Group chats in the Hermes Bots roster were rendered as a section header
("HERMES, PAPERCLIP MANAGER" + divider + tiny "Open chat" link) with each
member bot listed underneath — reading as separate DMs, not a group — and
opening the room rendered it INSIDE the narrow bots side panel.

Roster (plugin.js):
- Each group chat is now ONE standalone roster row (GroupRow), Discord-style:
  stacked member avatars (BotFace composite, org glyph when empty), group
  name, member count, latest room line as the preview with markdown syntax
  flattened, relative last-activity time, and the "needs you" badge on the
  row itself.
- The roster is a flat list: bot rows and group rows interleaved in the SAME
  pin+recency ordering (a group's recency = its newest room-log entry).
  Bots keep their individual DM rows; the group is an additional
  independent row. The groupRoster() sectioning is removed.
- New stripPreviewMarkdown() flattens **bold**, `code`, > quotes, links,
  and headings out of BotRow and GroupRow previews (raw ** and > were
  leaking into row previews).

Main-window takeover (sdk/index.ts + plugin.js):
- New generic SDK door host.openWorkspace(id, { render, title, minWidth,
  onClose }): registers a placement:'main' pane docked center into the
  workspace zone (the same shape session tiles and previews use), wires
  registerPaneCloser so tab Close / ⌘W tears the registration down, and
  reveals it. Re-opening the same id refreshes in place and re-fronts.
  Returns a disposer.
- Clicking a group row calls openGroupChat(): on desktops with
  host.openWorkspace the room opens as a tab taking over the MAIN chat
  area; older desktops keep the exact in-panel GroupChatWorkspace fallback
  (feature-detected, established Bot Mode pattern). Disband closes the
  main-window tab too; the composer/round-robin logic is untouched and
  renders identically in both hosts.

Tests: roster-groups rewritten for the flat model + new helpers;
group-chat/create-group-chat source contracts updated to the
openGroupChat door; profile-prewarm harness stubs stripPreviewMarkdown.
2026-08-17 16:39:59 -07:00
Teknium d16413ef43 fix(desktop): stop Skills Hub picker overlaying the installed list and cut Capabilities lag
Layout (overlap bug): the EmbeddedHubPicker section had no flex
containment — its fixed-height iframe viewport made the section's
min-content height rigid, so at a tall persisted drag height or a short
window the flex column starved the installed-skills area to 0px and the
hub header row painted straight over the list's sort strip and "changes
apply to new sessions" footer. The picker section now clips its own
content (overflow-hidden, shrinkable, min-h for the header row), the hub
viewport uses flex-basis instead of a hard height so it gives pixels
back first, the list region keeps a min-h-40 floor, and the sash drag is
clamped against the actual column height (list reserve) rather than just
the window.

Perf (Capabilities lag):
- The hub iframe (a full Docusaurus site) mounted eagerly with the view
  and re-mounted on every scope change (key={`picker-${scopeKey}`}).
  It now lazy-mounts the first time the Skills tab is shown, then stays
  mounted but display:none across Tools/MCP so a tab bounce never
  reloads the site. The scope key is gone — the picker fetches nothing;
  the profile rides into each install call as a prop.
- EmbeddedHubPicker is memo()ed and subscribes to the single
  UPDATE_ALL_KEY running flag via useStoreSelector instead of the whole
  $hubActions map, which churns on every tailed log line during installs.
- Collapse state now persists through the pane store (0 = collapsed),
  matching DetailPane, instead of resetting open on every mount.
- CapRow gets content-visibility:auto so 80+ row lists skip offscreen
  layout/paint (fixed row heights keep scroll geometry stable).

Validation: npm run check:lint (tsc x3 + eslint) clean; vitest
src/app/skills 9/9 passing, including a new test asserting the iframe
is absent on non-Skills tabs, mounts with the Skills tab, and survives
a tab switch hidden.
2026-08-17 16:39:52 -07:00
Teknium 76ff060748 fix(desktop): clean up Plugins settings — drop Example Plugin, hide bundled built-ins, add profile scoping
- Remove the bundled Example Plugin from the desktop renderer plugins
  (apps/desktop/src/plugins/example/). Reference/demo plugins live in the
  companion hermes-example-plugins repo (already pointed to by
  src/plugins/README.md); shipping the counter demo in everyone's Settings
  doubled UI noise for no user value.
- Agent plugins section now hides ALL repo-bundled built-ins
  (source === 'bundled': browser/browserbase, cron_providers/chronos,
  model-providers/deepinfra, platform adapters, image/video backends, …).
  The section is the control panel for plugins the user installed
  (user/git/project/pip/portable); built-ins ship enabled-by-default and
  are configured from their own surfaces. The HIDDEN_KEY_PREFIXES list
  stays as a fallback for older backends. Count pill reflects the
  filtered list.
- Add an "Applies to" profile scope selector to the Agent plugins section
  (same pattern as the Capabilities scope selector): list/toggle any
  profile's plugins without switching the whole app. Backend:
  plugins.manage now accepts an optional `profile` param via the same
  set_hermes_home_override contract as cron.manage / mcp.servers.*;
  unscoped calls are unchanged, so older backends keep working.
- i18n: new settings.plugins.agent.appliesTo key (types + en + zh; other
  locales fall back through defineLocale).
- Tests: plugins-settings.test.tsx covers bundled hiding, prefix fallback,
  count pill, selector visibility, scoped list/toggle payloads; new
  tests/test_plugins_manage_profile_scope.py mirrors the cron.manage
  profile-scope tests (scoped read, unknown-profile 4064, no override
  leak, unscoped contract unchanged).
2026-08-17 16:39:47 -07:00
Teknium 9a85769fc3 feat(desktop): unify Gateway + Connections into one Gateways settings page
Merge Settings → Connections into Settings → Gateway as a single
"Gateways" page:

- One nav entry ("Gateways"); the Connections nav entry is removed. The
  legacy `?tab=connections` deep link stays in the route enum and
  redirects to the unified page, and renders it in the interim frame so
  saved routes/bookmarks don't break or flash.
- The connections registry UI (list, add, edit, delete, test, make
  primary, update-all) moves into its own component
  (connections-registry.tsx) composed at the bottom of the Gateways
  page; connections-settings.tsx is deleted (nothing else imported it).
- The Add flow now offers ALL kinds: Local, Hermes Cloud, Remote
  gateway, and SSH (previously only remote/ssh). The Local kind button
  is disabled while the managed local entry exists; a hint points cloud
  adds at the sign-in/discovery flow above.
- Duplicate prevention, enforced in the save path on both sides of the
  IPC boundary (renderer inline error + main-process
  normalizeConnectionInput throw): only one 'local' entry ever;
  remote/cloud dupes keyed on the normalized URL (trim, strip trailing
  slashes, lowercase) across both kinds; ssh dupes keyed on normalized
  user@host:port + remote profile.
- The page-level "Applies to" ScopeChip row is gone. Per-profile
  gateway overrides are now an explicit "Per-profile overrides"
  subsection listing the default connection and each named profile,
  with an Edit affordance that drives the same scope state machinery.
- Command palette and profile-rail deep links retarget the unified
  page; i18n nav rename + new strings across types, en, ja, zh,
  zh-hant, ar.

Tests: new connections-registry.test.tsx (registry UI + dedupe helper
units, including the inline duplicate rejection), electron
connection-registry.test.ts dedupe cases, and updated
profile-rail-connect test.
2026-08-17 16:39:40 -07:00
Teknium 91878fb3c1 fix(desktop): visible eye catchlights on dark-bodied Bot Mode avatars
The BotFace catchlight dots were hardcoded white. On dark bodies
(maroon/ink/oxblood) isDarkColor() flips the pupils to light cream
(eyeFill), so a white catchlight on a cream pupil is invisible — those
avatars looked like they had no dots in their eyes (image14 report).

Catchlight contrast now follows the pupil, not the body: dark pupils
keep the white sparkle, light pupils get a dark one (rgba(0,0,0,0.6)).
The animation clock only moves the hb-hl elements (cx/cy) and never
touches fill, so the initial-render fill carries through every frame.

Verified with a rendered before/after/control strip: BEFORE maroon has
no visible dots, AFTER shows clear dark dots in the cream eyes, light
control body unchanged with white catchlights. Plugin suite green
incl. new face-catchlight.test.mjs.
2026-08-17 16:35:12 -07:00
Teknium 189de16280 fix(peer): live-verified cross-gateway bot DMs — session shape + virtual-model replay
Two bugs found by a REAL two-gateway live test (two isolated HERMES_HOMEs,
bravo running the api_server platform, alpha's agent autonomously running
`hermes peer dm` from its Bot Chat protocol; reply relayed correctly and
persisted in bravo's canonical Bot Chat):

1. peer dm parsed the session-create response flat, but api_server wraps
   the row: {"object": "hermes.session", "session": {...}} — every first DM
   to a fresh peer failed with "Peer did not return a session id" (and the
   orphaned Bot Chat then 400'd retries with duplicate-title). Parse the
   wrapped shape; the test fake now mirrors the real response shape so this
   class can't pass green again.

2. api_server: a session created with no model persists the advertised
   virtual model ("hermes-agent") on the row; session chat then replayed it
   as a REAL model id and the provider 400'd ("hermes-agent is not a valid
   model ID"). _request_agent_overrides already filters the virtual model
   for per-request bodies — apply the same filter to the stored session
   model at both chat sites (sync + stream), so it means "gateway default"
   exactly like the request-body path.

Live E2E transcript (bravo's Bot Chat, via /api/sessions/{id}/messages):
  user:      Message from 🤖 alpha (@alpha): What is your callsign?
  assistant: CALLSIGN-BRAVO-7
peer cmd unit suite 10/10 with the corrected fake.
2026-08-17 16:34:22 -07:00
Teknium 6f2dbf3a05 fix(gateway): widen scope-aware session DB resolution to the runner and release cached handles
Follow-up to the salvaged #88632 (Jack Lau) fix for #88532, extending the
same repair to the sibling frozen-at-init handle and closing the handle
lifecycle gap the per-path cache introduces.

1. GatewayRunner._session_db had the identical bug class: bound once as
   AsyncSessionDB(SessionDB()) in __init__ on the root home, while /resume,
   /title, /history and session search all execute inside
   _profile_runtime_scope on a multiplexed gateway. Convert it to the same
   property-with-pin pattern: per-access resolution of _default_db_path(),
   one cached AsyncSessionDB per resolved path under a lock, and assignment
   preserved as an explicit pin (many suites install fakes or None).
   Construction-time priming keeps the #88235 init-failure broadcast at
   startup.

2. Handle lifecycle: the per-path caches accumulate one open SessionDB per
   profile served, but the shutdown path closed only store._db /
   runner._session_db - which now resolve just the shutdown task's own
   (root) scope. Secondary profiles' handles would strand their WAL write
   locks until process exit, recreating the abandoned-handle leak
   b454e4da76 fixed and breaking --replace restarts with 'database is
   locked'. Add close_all_db_handles() / close_all_session_db_handles()
   sweeps and call both from the gateway teardown path. SessionDB.close()
   is idempotent, so the root handle being closed by both the legacy loop
   and the sweep is safe.

Tests: sweep coverage plus a runner-property scope/pin/cache test in
tests/gateway/test_multiplex_session_db_profile_scope.py.
2026-08-17 16:34:10 -07:00
Jack Lau 17ba992108 fix(gateway): resolve the session DB inside the active profile scope
Fixes #88532.

A multiplexed gateway serves every profile from one process, but
SessionStore bound a single SessionDB during __init__:

    self._db = SessionDB()

SessionDB(db_path=None) resolves _default_db_path() at call time and
does follow the context-local HERMES_HOME override, so the path
machinery was already correct. The problem was when it ran: at
construction, on the process's own root home, long before any inbound
event enters _profile_runtime_scope. Every profile's rows therefore
landed in the root state.db, even though the scope had redirected
get_hermes_home() correctly for the turn (that helper's own docstring
lists "sessions" among what it scopes).

The rows still carry the right profile_name, stamped from
source.profile by the same handler, so nothing in the data looks wrong.
The only visible symptom is the desktop listing a profile's session
under the default bot: _open_session_db_for_profile opens
profiles/<name>/state.db, which never received the write.

Look the handle up through a property instead, resolving the active
scope per access and caching one handle per resolved path so a hot
inbound path opens SQLite once per profile rather than once per
message. Construction stays under the cache lock so a concurrent first
message on a profile cannot open and then leak a second handle.

Assignment is preserved as an explicit pin, which is what the existing
suites rely on when they install a fake handle or disable the DB with
store._db = None, and a pin keeps winning across scope changes.

Behavior is unchanged when no profile scope is active, so single-profile
gateways resolve exactly the path they did before. This does not migrate
rows that already landed in the root store; those stay where they are.
2026-08-17 16:34:10 -07:00
Teknium 10134bd846 chore: map contributor email 2026-08-17 16:34:02 -07:00
BkashJEE 6c84c33546 fix(bot-mode): explain hidden cronjobs in the Routines pane empty state
Partial salvage of PR #88561 (routines-filter-hint half only; the pet
gallery pagination half is out of scope for #88263 and was skipped).

When the cron store has jobs but none surface for the active bot (older
gateway without profile scoping, no [bot:<name>] tags matching), the
Routines pane now explains that cronjobs exist but are hidden, instead
of showing the generic empty state — so users don't believe their
scheduled jobs disappeared.

Port of NousResearch/Hermes-Bot-Mode#95 (repo archived, ported upstream).
2026-08-17 16:34:02 -07:00
ScaleLeanChris 799d3fe021 fix(desktop): show legacy cronjobs in default bot
Signed-off-by: ScaleLeanChris <chris@scalelean.com>
2026-08-17 16:34:02 -07:00
Andrew Walker e3c4bfe9ab fix(bot-mode): show profile-owned cronjobs 2026-08-17 16:34:02 -07:00
Teknium aa8ceed4b6 fix(memory): keep a stale holder's late close() from evicting a fresh registry entry
Follow-up to the #88347 salvage: after release_all_under() force-closes a
profile's shared connection, a store re-created on the same path registers
a fresh entry under the same key. A stale holder that later calls close()
would pop that fresh entry (its refs were transferred nowhere), letting a
third store open a second connection to the same database — exactly the
multi-writer contention the shared registry exists to prevent. close()
now evicts the registry entry only when it is still its own.
2026-08-17 16:33:52 -07:00
liuhao1024 4f354c27b7 fix(profiles): release memory-store handles before rmtree on profile delete
The desktop's main serve process opens memory_store.db for every known
profile and nothing closed those connections before delete_profile's
rmtree — on Windows the open SQLite handles make the removal fail with
WinError 32 for both the CLI and the DELETE /api/profiles/<name> route
(#88347). POSIX unlinking of open files hid the same leak.

MemoryStore.close() is refcount-driven, so a live holder keeps the
handle forever; add MemoryStore.release_all_under(directory) to
force-close every shared connection under a directory, and call it in
delete_profile after stopping the profile backends. Inside serve the
handles live in that very process and get released; from the CLI it is
a no-op.

Fixes #88347
2026-08-17 16:33:52 -07:00
Teknium 399fdeae8b fix(bot-mode): plugin listener leaks, jsx key prop, OAuth poll overwrite
Three bugs in apps/desktop/src/plugins/hermes-bots/plugin.js:

1. register() subscribed host.state.profile / host.state.gateway listeners
   without capturing the unbind functions, so a plugin disable -> re-enable
   cycle stacked a duplicate listener per cycle that kept firing until app
   quit (same survives-disable class as the face clock before its
   onDispose hook). The unbinds are now captured and released via
   ctx.onDispose.

2. CreateRoutineDialog passed `key: createTarget` INSIDE the jsx() props
   object. The react/jsx-runtime silently ignores a `key` prop there (key
   is the third jsx() argument), so switching the routine owner never
   remounted the dialog and it kept stale per-bot form state. Moved the
   key to the third argument; the source-shape test now pins the correct
   shape and rejects the prop form.

3. beginOAuth() overwrote pollRef.current without clearing an existing
   interval, so a retry / double-click while a poll was live orphaned a
   2s poller that ran until unmount and could flip the phase from a stale
   OAuth session. It now clears any live poll before starting a new one.
2026-08-17 16:33:42 -07:00
Teknium aa2622128f fix(hermes-bots): show profile pics in group chat rooms
Community report: group chats showed neither profile pictures nor proper
names for members (names fixed in #88721; this adds the avatars).

- Every bot message in a group room now carries the speaker's avatar,
  resolved through the same botAppearance pipeline as the roster: custom
  uploaded/generated/pet images honored, backfilled PNGs dropped so the
  animated math face renders, deterministic shape+color for bots with no
  customization (including remote speakers with no local meta).
- The room header shows the member roster as overlapping faces (capped
  at 6) with a display-name tooltip, alongside the existing count.
2026-08-17 16:29:45 -07:00
Teknium 9b112e5736 refactor(desktop): extract pool teardown into pool-stop.ts with behavior tests
Follow-ups on the salvaged bounded-teardown fix:

- Replace the source-grep regression test (backend-lifecycle.test.ts asserted
  main.ts source text) with behavior-contract tests against the extracted
  createPoolStopper(): handle retention until bounded exit, stop dedup,
  stopAll completeness, and the respawn-awaits-dying-backend gate.
- Gate the registry-local spawn path on the in-flight stop too (sibling site
  the original PR predates).
- Route teardownPoolBackendAndWait through the shared stopper so profile
  delete/rename teardown covers both local pool keys with the same bounded
  semantics.
2026-08-17 16:26:02 -07:00
giga corp c07171209a fix(desktop): await pooled backend teardown 2026-08-17 16:26:02 -07:00
Teknium c6a0789bc5 fix(desktop): retire old-name renderer sockets and widen the deletion gate for renames
Follow-ups on top of the salvaged rename lifecycle:

- Retire both local renderer socket scopes for the OLD profile name before
  the rename PATCH (rename-profile-dialog), mirroring the delete-path fix
  from #88638 — a retained socket otherwise treats the rename's backend
  teardown as a transient drop and its reconnect respawns the old-name
  backend, whose ensure_hermes_home() recreates the directory the rename
  just moved (#45474).
- Hold the profile deletion gate for rename requests too, so a concurrent
  renderer reconnect entering ensureBackend() mid-rename cannot respawn the
  old-name backend.
- Regression tests for retire-before-rename ordering and the
  validation-rejected path.
2026-08-17 16:15:22 -07:00
wangdong 69abe41f36 fix(desktop): re-home backend after profile rename 2026-08-17 16:15:22 -07:00
Teknium 4b7eb02907 fix(hermes-bots): group chats never surface @default — Hermes keeps its name
Community report: in a Bot Mode group chat with the primary agent, Hermes
lost its name and rendered as @default in the room.

- Workspace speaker labels now render the roster displayName (default →
  Hermes, titles respected) instead of the raw @profile id. Clicking a
  speaker reveals the full disambiguated form — Display-gatewayname
  (@handle) — so same-named agents on two connections stay tellable
  apart on demand; naturally they just show their display name.
- Room transcripts fed to members render the default profile as Hermes
  (new groupSpeakerLabel helper).
- The per-turn prompt addresses members by @handle (@hermes for the
  primary profile) instead of @default, and parseGroupChatMentions now
  resolves @hermes back to the default member via botHandle.
2026-08-17 16:14:53 -07:00
Teknium 6229683b62 feat(cli): hermes peer — bot-to-bot DMs across machines and gateways
Bots could message teammates on their own machine (hermes -p <bot> chat) and
the desktop could relay user mentions over Connections, but a bot had NO
transport to a bot on another gateway. This adds one, with zero new server
surface: the peer's existing api_server platform is the wire.

- hermes_cli/subcommands/peer.py: `hermes peer add/list/remove/dm`.
  `dm <peer>[/<agent>]` resolves the remote agent's canonical "Bot Chat"
  (list by title, create when missing), runs one synchronous agent turn via
  POST /api/sessions/{id}/chat, and prints the reply on stdout — the exact
  cross-machine twin of the local bot-messaging command, so the Bot Mode
  protocol composes over it unchanged. Named profiles route via the peer's
  /p/<profile>/ multiplex mirror. Peer URLs live in config.yaml
  (`bot_peers`); the peer's API_SERVER_KEY is a credential and lives in
  ~/.hermes/.env as HERMES_PEER_<NAME>_KEY.
- hermes_cli/main.py: parser wiring + fast-path/session-flag command sets.
- tools/bot_mode_probe.py: when peers are registered, the injected Bot Chat
  messaging protocol gains a cross-machine paragraph (peer roster +
  `hermes peer dm` pattern) so agents discover remote teammates on their
  own; peers join the capability fingerprint so registering/removing one
  refreshes eternal Bot Chat prompts on the next message (loud, one-time,
  user-initiated — no per-turn cache drift).
- Docs: Bot Mode guide (bot-initiated DMs across machines) + cli-commands
  reference (`hermes peer` section + summary row).

Tests: tests/hermes_cli/test_peer_cmd.py (target parsing, /p/ scoping,
registry round-trip in isolated config, real-loopback-HTTP dm flow incl.
Bot Chat create-vs-reuse and bearer auth), bot_mode_probe peer-paragraph +
epoch tests. E2E: real `python -m hermes_cli.main peer ...` against a live
fake peer over HTTP with isolated HERMES_HOME (config/.env persistence,
bare + /p/<profile> routing, stdin, --json). 23 passed; ruff clean.
2026-08-17 16:13:30 -07:00
Teknium 66221397a1 fix(bot-mode): always hide Bot Mode sessions from the global Sessions sidebar
Bot Mode's group chats spawned one per-member session per room, and those
"Group: ..." rows (plus canonical Bot Chats when the old eye-toggle pref was
off) flooded the global Sessions sidebar — a 6-bot room dumped six identical
rows into recents (reported with screenshot, Aug 17).

Plugin (apps/desktop/src/plugins/hermes-bots/plugin.js):
- session.create now passes hidden:true UNCONDITIONALLY for both canonical
  Bot Chats and group-room member sessions; the $hideBotChats pref, its eye
  toggle, and its storage hydrate are removed (Bot Mode sessions are plumbing
  or plugin-owned forever-chats, never scratch conversations).
- hideOwnedBotSessions(): idempotent reconciliation sweep over every owned
  session id (bot meta canonical chats + each room's member sessions) via
  session.set_hidden, run on plugin load and on each gateway reconnect, so
  rows born visible under the old pref get cleaned up.
- The Bots session browser and canonical-chat recovery scan pass
  include_hidden:true so they still see the rows they own.

Gateway (tui_gateway/methods_session.py):
- session.list honors an include_hidden param (default off — the resume
  picker and all global callers keep dropping hidden rows).
- session.set_hidden gains a durable fallback: when no LIVE runtime session
  matches, resolve the stored session id in the target profile's state.db
  (via resolve_session_id) and flip the flag there. The sweep holds stored
  ids for chats that aren't live; the old live-only lookup 4001'd them.

Validated E2E with real imports against a temp HERMES_HOME: born-hidden row
(hidden=1), profile-scoped session.list default vs include_hidden (0 vs 1),
and stored-id sweep on a non-live legacy row (hidden=1). Plugin suite
167/167; new RPC regression tests in tests/tui_gateway/test_session_hidden_rpc.py.
2026-08-17 16:02:42 -07:00
hermes-seaeye[bot] bb92473074 fmt(js): npm run fix on merge (#88720)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-17 22:59:42 +00:00
embwl0x 55e34fb7d0 fix(desktop): avoid local profile REST backend spawns 2026-08-17 15:51:02 -07:00
adybag14-cyber 51de5c91b5 fix(termux): preserve Nix update guidance 2026-08-17 15:47:17 -07:00
adybag14-cyber 9e81a102df chore: map contributor email 2026-08-17 15:47:17 -07:00
adybag14-cyber d685ea4df5 docs(termux): document community pkg distribution 2026-08-17 15:47:17 -07:00
adybag14-cyber 26466d0594 feat(termux): support apt-managed Hermes installs 2026-08-17 15:47:17 -07:00
Gille 976183f536 fix(desktop): stop deleted profiles reconnecting 2026-08-17 15:46:44 -07:00
Gille 68a713385e fix(gateway): preserve configured context window 2026-08-17 15:46:23 -07:00
Teknium 2c8a2b65aa docs(sdk): document profiles.list preferred_session_ids lookup
Covers the precise pinned-session resolver added in PR #88690: request
param shape, the preferred_session response field, hidden-row and
compression-lineage resolution, and older-gateway behavior.
2026-08-17 15:35:36 -07:00
Teknium 21aa327c69 style: ruff lint + format on the eval scripts (encoding args, formatting) 2026-08-17 15:29:16 -07:00
Teknium 5033aedaa3 feat(evals): add the Browser Use mode A/B benchmark from PR #81958
Reconstructs the 204-run benchmark battery behind #81958 (Browser Use CLI
3.0 mode) as a rerunnable eval under evals/browser-use/, following the
toolperf_abeval / evals-compaction pattern.

- tasks/easy.json + tasks/hard.json: the oracle-checked toscrape task
  batteries (5 easy, 6 hard) exactly as run for the PR
- single_run.py: one cell = task x arm (base | pr | prns) x model x rep;
  throwaway HERMES_HOME, web-fetch creds stripped, arms pinned to separate
  trees via BUBENCH_BASE_TREE / BUBENCH_PR_TREE
- orchestrate.py: resume-safe local-CDP battery driver
- orchestrate_cloud.py: backend matrix (nous-cloud via the browser_use
  provider plugin, browserbase via REST) with per-cell session lifecycle
- report.py: scorecard aggregation with vs-base token deltas
- README.md: design, run instructions, and the recovered Aug 8-10 2026
  baseline scorecards (hard battery, backend matrix, easy round 1,
  digest ablation)

The original /tmp/bu-bench workspace was lost to a tmpfs reboot; harness
and readouts were recovered verbatim from the benchmark session's tool-call
history in state.db, with hardcoded paths parameterized. Smoke-verified
live: report.py aggregation, and single-cell runs (pr + base arms) against
a real headless Chrome CDP with sonnet-5 driving browser_exec, oracle pass.
2026-08-17 15:29:16 -07:00
Teknium 4151599e81 fix(desktop): cross-machine bot DMs — canonical chat, sender attribution, reply relay
Follow-up to #88664. The remote @mention delivery path had three gaps that
made cross-machine DMs half-work:

1. No reply relay: deliverRemoteRosterMentions submitted the prompt and
   toasted, but never polled — the handoff note promised a relay that never
   came. Now a bounded poll (same shape as a group member turn: new
   assistant message after the baseline, 180s cap) relays the recipient's
   reply as a notification, or says it's still pending.
2. No sender attribution: the raw user text was submitted, so the
   recipient's messaging protocol never recognized an agent-to-agent
   message. Deliveries now carry the standard
   "Message from 🤖 <sender> (@handle):" prefix.
3. Duplicate Bot Chats: every mention minted a fresh "Bot Chat" session.
   ensureRemoteCanonicalChat now resolves the recipient's pinned canonical
   chat from its profile ui_meta, falls back to resume-by-title, and only
   creates when neither exists — mirroring ensureGroupChatSession.

Tests: remote-dm-delivery.test.mjs (pin-resume without create, attribution
prefix + reply relay via vm-run behavior tests, source contract for the
bounded poll). Plugin suite 187/187.
2026-08-17 15:28:43 -07:00
Teknium d0b02efa9d fix(desktop): don't treat a mid-switch 404 as session deletion (#88540)
A resume racing a profile/connection swap can 404 on a backend that
does not own the session; the terminal-failure branch then dropped the
window to the blank new-chat route while the target session was alive.

goneSessionVerdict() now gates the draft fallback: a session created
this run, still listed on some profile, or looked up while a gateway
swap is in flight arms the bounded auto-retry latch instead of
discarding the route. Draft remains the calm-conditions path for
verifiably dead ids.
2026-08-17 15:27:00 -07:00
NealZhouPanda 8484786832 fix(desktop): align BOTS roster preview with the session its click opens
The BOTS sidebar previewed each profile's most recently active session
(last_session) but clicking the row opened the pinned canonical chat —
two different session identities, so the preview described one
conversation and the click landed in another.

- profiles.list gains an optional preferred_session_ids param
  ({profile: session_id}): an exact, existence-checked per-profile
  lookup that resolves hidden rows and compression lineages to the
  live tip (the same resolver session.resume uses) and returns a
  preferred_session summary alongside the unchanged last_session.
- The hermes-bots plugin sends its canonical-chat pins with each
  roster poll and previews preferred_session ?? last_session.
- openBotCanonicalChat verifies pins through the precise resolver
  instead of a paginated, hidden-excluding session.list window that
  misjudged real hidden pins as gone; transient lookup failures no
  longer clear the pin or mint a replacement chat.
- Grandfathering: a bot with history but no pin adopts the previewed
  session on first open instead of minting a new empty chat — the
  behavior the design comment already promised.

Closes #88200
2026-08-17 15:26:48 -07:00
Adolanium 69a62ce2c8 fix(desktop): keep the Bot Chat pin
A failed intro used to clear the pin even though the chat was already made, so the next click opened a second one. A missing pin grabbed the newest session, even when a real Bot Chat was in the list.

Failed intros now keep the pin. A missing pin looks for a session titled Bot Chat. If that is not there, we try the stored pin instead of the newest row.

Ported from NousResearch/Hermes-Bot-Mode#59 after Bot Mode moved in-tree.
2026-08-17 15:26:48 -07:00
hermes-seaeye[bot] dc2e9dde9a fmt(js): npm run fix on merge (#88671)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-17 21:39:36 +00:00
Teknium bf53915331 docs: Bot Mode guide — Create-on picker, cross-machine mentions + group chats
Community asks (Discord, Aug 17): unclear what happens across cloud vs
desktop, whether every bot replies in group chats, and how to persist
connections to multiple gateways.

Extends the Bot Mode user-guide page (landed on main today) with the new
cross-connection features:

- "Create on" picker: creating an agent on another registered machine, with
  the remote-target caveats (clone source, staged capability checklists,
  draft discard).
- Group chats: explicit "not every bot replies" explanation of the
  round-robin/pass model, and rooms spanning machines with device badges.
- @mentions across machines via the Connections registry (no gateway switch).
- Bots-across-machines section: persistent SSH inventory, last-known rows,
  and the stay-in-your-chat interaction model; cloud+desktop recipe.
- desktop.md Bot Mode section links to the full guide; multi-connection
  page's Bot Mode reference points at the docs page instead of the old
  standalone repo.
2026-08-17 14:32:52 -07:00