Commit Graph

40401 Commits

Author SHA1 Message Date
teknium1
d9ca819cc4 Inspired by Amp: pick the workspace a dashboard chat starts in
A fresh dashboard /chat always spawned the TUI in the dashboard process's
launch directory, so from a phone or any browser there was no way to aim
a new session at a specific repository. Amp's runners now serve many
directories and the web composer offers a picker of the runner's projects
and discovered git checkouts; this ports that mechanism onto the surface
Hermes already has: the dashboard is the phone/web front, the host's
projects.db + repo-discovery cache are the served directories.

- GET /api/chat/workspaces: the profile's projects (with folders) and
  discovered repos (session-derived + scanned), default_cwd, home;
  ?scan=1 rescans desktop.repo_scan_roots on the host, so headless
  installs (no Desktop to populate the cache) discover repos too.
- /api/pty?cwd=<dir>: validated (existing directory, fail-closed 400
  via the PTY error path) and forwarded to the TUI child as HERMES_CWD
  (self-spawned gateway cwd) + HERMES_TUI_CWD (explicit cwd on
  session.create for the dashboard's in-memory gateway, whose own cwd
  is the launch dir). Resumed sessions ignore it.
- ui-tui: session.create carries cwd when HERMES_TUI_CWD is set, so
  /new inside a dashboard chat stays in the picked workspace too.
- web: workspace selector in the Chat rail above "New chat" (projects,
  repos by recency, Other path…, rescan), remembered per profile in
  localStorage; 17 locales.
- docs: web-dashboard.md rail + REST sections.

Live E2E: real `hermes dashboard` under a scratch HOME with two git
repos under desktop.repo_scan_roots -> /api/chat/workspaces?scan=1
lists both; /api/pty?cwd=repo-a -> TUI status bar shows ~/code/repo-a;
/api/pty?cwd=<missing> -> "Working directory does not exist" + close.
2026-09-23 03:12:04 -07:00
teknium1
a2fbbf482d chore: map salvaged contributor emails 2026-09-23 03:10:15 -07:00
teknium1
b3624cc3af docs(imessage): hints match the stripped-on-link send path
Photon messages with a link now arrive stripped rather than raw, and BlueBubbles
keeps link URLs, so the hints stop describing the old leaks.
2026-09-23 03:10:15 -07:00
teknium1
73ba639092 fix(imessage): strip markdown on every Photon plain send and keep link URLs on BlueBubbles
Follow-up on the two salvaged commits:
- One _sidecar_payload_text() helper decides the /send text for both the adapter
  path and _standalone_send (cron / send_message), which had the same raw-markdown
  leak on URL-bearing messages and never stripped with PHOTON_MARKDOWN=false.
- BlueBubbles (same iMessage surface) keeps [label](url) targets as bare URLs.
- Tests trimmed to two invariants.
2026-09-23 03:10:15 -07:00
Semir Kabir
ccfb68688e fix(photon): stop advertising fenced code-block support on iMessage
The gateway's tool-progress path emits terminal commands as fenced code
blocks on any adapter whose supports_code_blocks is True. The Photon
adapter set that flag from PHOTON_MARKDOWN, but the sidecar's /send
router (send-format.mjs) silently routes every URL-bearing message
through the plain-text builder — where fences survive as literal backtick
characters — and even the markdown path renders a fence as inline
monospace text, not a block. Net effect: raw fenced terminal commands
(and raw markdown around them) surfaced in iMessage bubbles no matter
what the user's prompt-level style rules said.

Fix the whole class: the adapter never claims code-block support, so the
gateway emits its compact one-line tool preview instead. Prose markdown
passthrough (bold/italic/headings) is unchanged, and no config change is
required — tool_progress can stay at its user's preferred mode.

Tests: new capability + E2E tests pin that the gateway cannot emit a
fence for Photon with real adapter + real display resolution; the old
supports_code_blocks-mirrors-env expectation (which pinned the buggy
behavior) now asserts the flag is always False.
2026-09-23 03:10:15 -07:00
Eddie Wang
449ac2fb8f fix(photon): strip markdown when the sidecar downgrades to the text builder
chooseSendFormat() routes markdown containing a raw http(s) URL through
spectrum-ts' text() builder, because the markdown builder's iMessage data
detection 500s on those messages (#73615). text() ships the payload
verbatim, and nothing strips the markers on the way down, so any reply
that mentions a link arrives in iMessage as literal markdown source:

    **Release 1.2.0** is out
    - **EUR 5** off this month
    https://example.com/releases/1.2.0

Every ** is visible in the bubble. Remove the URL from that same reply
and it renders correctly, which is what makes the URL the trigger rather
than the content.

spectrum-ts documents markdown() as degrading to readable plain text on
platforms without native support "instead of surfacing raw ** markers".
Selecting text() ourselves opts out of that guarantee, so the adapter has
to honour it instead.

Strip in _sidecar_send() when the payload is markdown and the sidecar will
downgrade it. The format key is deliberately preserved: the sidecar owns
the builder choice (test_rich_links.py pins that contract), and an older
sidecar without chooseSendFormat must keep rendering natively.

_send_plain_fallback() selects the text builder explicitly via
markdown=False and had the same leak, so it strips too.

Reuses the shared strip_markdown() helper rather than adding a second
implementation, so the PHOTON_MARKDOWN=false path and this path produce
identical output and both inherit any future fix to that helper.

Diagnosis previously reported in #85733, which was closed unmerged.

Stripping must not take the URL with it, though. The shared helper
collapsed [label](url) to label alone, which on this path is worse than
raw markdown: iMessage auto-links bare URLs and nothing else, so the
reply arrives with a description and no way to reach the link.

    Open the itinerary on Google Flights     <- URL gone entirely

So strip_markdown() takes keep_link_targets, which rewrites
[label](https://url) as "label\nurl" (own line, because these URLs are
often long) and leaves non-http targets such as mailto: or relative
paths label-only, since iMessage won't linkify those either. The default
is unchanged, so the SMS, IRC, Feishu and QQ callers keep dropping the
target as before.

plugins/platforms/line/adapter.py already carries a private
strip_markdown_preserving_urls() for exactly this reason ("LINE
auto-links bare URLs only"). This moves the behaviour behind the shared
helper instead, so Photon's three plain-text paths -- the downgrade,
_send_plain_fallback(), and PHOTON_MARKDOWN=false -- all agree.

gateway/platforms/bluebubbles.py is the same iMessage surface with the
same loss; left alone here to keep this change to one platform.
2026-09-23 03:10:15 -07:00
teknium1
6fa6df75f2 fix(imessage): platform hints steer replies to plain texting style
The Photon hint told the model "Markdown is rendered (bold, italics, lists,
code)", so replies came back with headers, code fences and backticks. That
is wrong on the real delivery path: the sidecar sends any message containing
a URL as raw text (every *, #, ``` and | shows literally), and even on the
markdown path spectrum-ts flattens headings to bold, tables to "a | b"
rows, and turns code into Unicode math-monospace glyphs that break when
copied. The hint also claimed attachments are metadata-only, which stopped
being true when native media send landed.

Both iMessage hints (Photon plugin, BlueBubbles built-in) now ask for a
texting register: short, answer first, no headers/tables/fences/backticks,
commands on their own plain line so they copy, bare URLs. BlueBubbles also
notes that strip_markdown drops the URL of [text](url) links.
2026-09-23 02:36:34 -07:00
teknium1
54c55307e3 test(desktop-e2e): never run a local e2e against the developer's real Hermes
buildAppEnv sandboxed HERMES_HOME only. Desktop now attaches to the host's
running hermes serve (one backend per host) and profile roots are
HOME-anchored, so on a machine running Hermes a local spec either attached
to that backend or listed the real profiles, and its sends went through
the developer's real model and state.db instead of the mock provider.

Set HERMES_DESKTOP_ISOLATED_BACKEND=1 (the documented private-backend
escape hatch) and HOME=<sandbox root>. CI has neither a running backend
nor real profiles, so it never took this path.
2026-09-23 02:28:38 -07:00
teknium1
680d613a01 fix(desktop): live-reload only loopback dev pages, one loopback predicate
Follow-up to the salvaged fix. Narrow the workspace live-reload gate to
loopback (full 127/8, ::1, 0.0.0.0, localhost) and drop `.local`: mDNS
names are other devices on the LAN (homeassistant.local), the same
"unrelated page loses its state on every agent file edit" class the fix
closes. The pane's error copy already had its own loopback regex for the
remote-gateway hint; both call sites now share isLoopbackPreviewUrl.

Tests trimmed to one pane invariant (reloads a loopback page, stops once
the tab browses elsewhere: the live page decides, not the tab's original
address) plus the predicate table.
2026-09-23 02:28:17 -07:00
fangliquanflq
8206d89b98 fix(desktop): preserve external preview page state 2026-09-23 02:28:17 -07:00
teknium1
3fb8b3f65f fix(models): a curated fallback never pins itself in the provider models cache
When a provider's live catalog fetch failed, provider_model_ids() degraded to the
curated static list and cached_provider_model_ids() wrote that list to
provider_models_cache.json with a fresh 1h TTL, exactly as if it were the
account's real catalog. The "only non-empty results are cached" guard never
fired because the fallback is non-empty. A transient Copilot outage therefore
replaced an account's 10 enabled models with the 17-model static list on every
picker surface until the TTL lapsed, and a same-credentials restart, re-auth or
Refresh Models could not shake it (#107391).

Mark the curated list as CuratedFallbackModels at the sites that serve it for a
missing live catalog (the Copilot fetcher, the generic profile merge, and the
static tail when a live fetcher declined). The cache layer then treats it as a
placeholder: it never replaces a same-credentials live row (the account's real
catalog is served instead), it is stored flagged with a 60s TTL when there is
nothing better, and it is never served through the stale-while-revalidate
window. A provider with no live source at all is unaffected: its static list is
its catalog and caches as before.
2026-09-23 02:19:11 -07:00
teknium1
055c69d48f plugin-catalog: claude-subscription-directsdk pin -> 815c05e7 (Opus 5.5 route)
Picks up the plugin's merged fix: Opus 5.5 is a pinned 1M-context route and the
`opus` alias resolves to it (plugin PR #19). Plugin version unchanged (0.3.0);
the card art URL follows the pin.
2026-09-23 02:18:51 -07:00
hermes-seaeye[bot]
a27b13056e fmt(js): npm run fix on merge (#120048)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-23 09:02:52 +00:00
teknium1
71b61884a4 fix(desktop): deliver a gateway event once even when two sockets to one backend carry it
The backend stamps `seq` once per event before its transport fan-out, so the
same frame arriving on two of the renderer's sockets to one process carries
the same (session_id, seq); `JsonRpcGateway` is per socket and cannot see the
other copy. `JsonRpcGateway.dispatchEvent` now tags each event with the
`replay_epoch` its socket adopted from `gateway.ready` (per process, not per
socket), and both fan-ins in `use-gateway-boot.ts` (primary + registry
secondaries) pass through one `createGatewayEventDedupe()` gate keyed by
(epoch, session_id, seq) before any store runs.

A duplicate is the same key within 30 s. Not "seq not above the highest
seen": the backend restarts a session's counter at 1 when the session leaves
its 64-session replay ring, and a high-water mark would swallow the whole
restart of any short session. Seq-less / session-less events always pass.
Bounded: 2048 seqs per session, 256 sessions LRU.

Live, with the router fix reverted so two sockets still join the chat: the
DOM never doubled a word (0/38 samples vs 4/40 on main) and the doubled
"reply was cut short" card is back to one.

Closes #120007. Part of #120005.
2026-09-23 01:56:12 -07:00
teknium1
0bb539b472 fix(desktop): session calls for a profile on the shared host backend ride the primary socket
Since #118246 one local `hermes serve` serves every profile, and main marks
those routes `sharedPrimary: true`. `requestGatewayForProfile` honoured the
flag, but the session-owner family (`requestGatewayForAgent`, retain/open/
ensure) went through `isAttachedSharedRemote`, whose `primaryConnectionMode
=== 'local'` early return (#113956, written while every local profile had
its own pooled child) sent every non-default local profile to a registry
secondary: a SECOND WebSocket to the SAME backend process. The backend joins
any socket that sends a session call to the chat's transport fan-out, so the
renderer received every event twice — "HelloHello from from the the mock
mock…" while streaming, duplicate interim bubbles, doubled error cards
(#120005, #119131, #119566, #119540, #118934).

The predicate is now `ridesPrimaryBackend`: it asks main for the route on
every call and reuses the primary socket (with the `profile` param) when the
descriptor carries `sharedRemote` (#96493) OR `sharedPrimary` (#118246).
#101416 stays fixed: when the probe fails on a LOCAL primary we never fall
back to the primary (a pooled profile's chat must not be minted under the
primary's pid), and a pooled descriptor (HERMES_DESKTOP_ISOLATED_BACKEND=1)
still opens its own secondary.

Closes #120006. Part of #120005.
2026-09-23 01:56:12 -07:00
teknium1
4a2bd7406e fix(desktop): models added by a plugin or catalog update no longer start hidden in the picker
The Edit Models store persisted only an allowlist of visible provider::model keys.
Once a provider had any stored key it was skipped by the default expansion, so a
model that appeared later (plugin update shipping a new route, catalog refresh,
new release) was absent from the list and rendered switched off. The store could
not tell "hidden on purpose" from "never seen".

Persist a `known` snapshot (hermes.desktop.known-models) beside the allowlist,
recorded whenever the user persists a choice. A curated provider now admits
models absent from the snapshot through the same curated default rule
(featured list / top-N); a provider the user hid outright stays hidden, new
models included. Stores written before the snapshot existed adopt the catalog
as judged the first time it loads, so existing hide choices are honoured
verbatim and only later arrivals count as new.
2026-09-23 01:35:20 -07:00
teknium1
9d799e0531 docs: hindsight installs from the plugin catalog
memory-providers.md points the Hindsight section at the catalog entry and the
upstream integration docs, documents the auto-install on `hermes update` /
first agent start (and the allow_lazy_installs=false one-liner), and adds a
"Migrating from bundled Hindsight" subsection. memory.md, overview.md,
integrations/index.md, cli-commands.md, docker.md and nix-setup.md no longer
say hindsight ships in-tree or as the [hindsight] extra.
2026-09-23 01:16:41 -07:00
teknium1
090b06c345 test: drop the bundled-hindsight tests, retarget the generic ones
Deleted: the seven files whose subject was plugins/memory/hindsight itself
(provider, config schema, env perms, local-runtime hint, templates, health
grace timeout, root guard) and the hindsight-only cases inside the multiplex
identity-scope, provider-thread, BOM-tolerance and session-switch suites.

Retargeted: the dashboard memory-provider config-surface tests used hindsight
as the only bundled provider with a flat `<home>/<name>/config.json` declared
schema; they now install a synthetic user plugin (`flatprov`) into the isolated
HERMES_HOME so the generic declared/live PUT + GET paths stay covered. The
lazy_deps plugin-owned-range test keeps mem0ai as its subject.
2026-09-23 01:16:41 -07:00
teknium1
73c598e319 build: drop the hermes-agent[hindsight] extra
The catalog plugin declares hindsight-client in its own plugin.yaml and the
plugin installer resolves it (into HERMES_LAZY_INSTALL_TARGET on the sealed
Docker image), so the pyproject extra, its exclude-newer entry and every
consumer that pre-installed it — the Dockerfile, the nix full package /
module examples / check override, and the CI `uv sync` matrices — stop naming
it. uv.lock regenerated with `uv lock` (only the hindsight-client package and
the extra leave the lock; the exclude-newer block is re-sorted by uv).
2026-09-23 01:16:41 -07:00
teknium1
4cbf862abe chore(memory): remove the bundled hindsight provider (moved to the plugin catalog)
Hindsight now ships from its maintainer's repo (vectorize-io/hindsight,
hindsight-integrations/hermes) through plugin-catalog/hindsight.yaml, so the
in-tree copy under plugins/memory/hindsight goes away. Homes that still name
`memory.provider: hindsight` are migrated by hermes_cli/memory_provider_migration.py
(the `hermes update` hook and the first agent start install the catalog plugin);
that path is untouched here.

Core-side special cases that only made sense with the bundled copy go with it:
the `memory.hindsight` LAZY_DEPS feature, `_provider_pip_dependencies`'s
`hindsight-all` expansion in `hermes memory setup` (the plugin's own
`_ensure_local_runtime()` installs the embedded runtime now), the compat-manifest
pointers of the deleted module, and prose that listed hindsight among the in-tree
providers. Generic provider-name lists, the HINDSIGHT_* env hints and the API-key
redaction pattern stay — a catalog-installed hindsight still uses them.

Revert this commit alone to restore the bundled provider.
2026-09-23 01:16:41 -07:00
John Paul Soliva
526d135a96 fix(compression): keep the /compress here N tail in state.db under in-place compaction
compress_now() hands only the head to _compress_context() and rejoins the
kept exchanges in memory afterwards. With compression.in_place (the
default), the commit's archive_and_compact() archives every active row at
or below the lease watermark, which includes the kept tail's rows, and
inserts only the compacted head. Its tail_count rewind then lands on the
newest rows under the watermark, which are the kept tail itself, so those
rows end up active=0, compacted=0 with no live copy. No surface writes them
back: the CLI re-flushes only after a rotation, the gateway skips in-place
on purpose, and the TUI only swaps its in-memory history. The exchanges the
user asked to keep verbatim are gone on resume, and on the gateway from the
very next message.

compress_now() now passes copies of the tail as verbatim_tail. The in-place
commit stores head + tail in the same archive_and_compact() transaction,
joined with the same seam rejoin_compressed_head_and_tail() builds in
memory, and adds the tail to tail_count. The rewind flags then land on the
kept tail's originals and on compress()'s own carried rows, which were
left compacted=1 and shown twice in the resumed display history. The
copies are stamped as persisted and compress_now() returns the stored list
instead of rejoining the tail a second time. Rotation, no-op and
rolled-back commits are unchanged: the copies stay unstamped and the
caller's tail is rejoined as before.
2026-09-23 01:11:58 -07:00
teknium1
81a095cb1d catalog: honcho (Plastic Labs) and supermemory (Supermemory) memory providers from their maintainers' repos
First two of the built-in memory providers whose maintainers now own the plugin. Same provider
name, config section, data and tools as the in-tree copies, so a later removal from core migrates
users via hermes_cli/memory_provider_migration. Nothing is removed here.
2026-09-23 01:08:19 -07:00
teknium1
344c522731 catalog: hindsight memory provider from vectorize-io/hindsight (subdir hindsight-integrations/hermes)
Pins the maintainer-owned copy of the formerly bundled plugins/memory/hindsight at
dc75038866a3966b5bf5c82ff57c7a758bb4070a (tree identical to tag v0.10.1). Same provider name,
memory.hindsight section, data dir and tool names, so hermes_cli/memory_provider_migration.py
can install it for existing users once the bundled copy leaves core.
2026-09-23 01:07:57 -07:00
teknium1
9318b7a74d fix(memory): user-dir memory providers keep Desktop config + OAuth
A memory provider installed from the catalog under $HERMES_HOME/plugins/
(the honcho handoff package) loads under the loader's synthetic user
namespace, but the host-side surfaces still imported the bundled path
`plugins.memory.honcho.*` by name: the Desktop GET/PUT
/api/memory/providers/honcho/config 500'd, /oauth/start|status 404'd
("does not support OAuth connect"), doctor reported "honcho-ai not
installed", and profile clone / post-update sync silently skipped.

Add `plugins.memory.import_provider_module(name, submodule=None)`: the
provider package (or one of its submodules) of whichever copy
`find_provider_dir` resolves — bundled today, user dir after the removal
PR — under the module name the loader already owns, so both copies
behave identically. Route every host-side absolute import through it:
the dashboard host-block resolvers and honcho.json writer, the OAuth
route resolver, doctor's honcho/mem0 checks, `profile create --clone`,
the update hook's profile sync and the holographic store release.
In-process, no lock, no child process.

Two invariant tests (user-dir honcho with the bundled copy gone:
/config?surface=declared is 200 with the schema; the oauth_flow module
resolves from the user directory), red on base. The honcho write test's
`_honcho_resolvers` stub gains the provider-name argument the seam now
carries.

Shape follows the host-module resolution slice of #116566 by @erosika;
the contract module, installer lock and child-process OAuth runner from
that PR are not needed once the modules resolve in-process.

Co-authored-by: Erosika <eri@plasticlabs.ai>
2026-09-23 01:07:08 -07:00
Teknium
5908e1aaa8 test: trim memory write-bridge regression tests to the invariants
Keep the two tests that are red on unchanged main: previous_content comes
from the locked native-store result (single + batch replace/remove, batch
ordering) and never from caller arguments or build_metadata. Drop the
uncommitted-batch cases (they pass on main -- the no-notification gate on a
failed write predates this change) and the memory/user target axis (same
store code path).

Salvage of #118903 by @ehz0ah.
2026-09-23 01:06:32 -07:00
ehz0ah
d57c2a3254 fix(memory): forward committed entry identity to providers 2026-09-23 01:06:32 -07:00
teknium1
38c289c014 fix(models): drop gpt-6-terra, a tier OpenAI never published
#119410 registered gpt-6-terra alongside sol and luna from the request text, and the Codex
forward-compat synthesis put it (and -900k) in the live /model picker on every surface.
Nothing serves it: not the Codex account catalog (astra, sol, luna), not OpenRouter (same
three, plus -pro), and OpenAI's model page 404s. Forward-compat is for published tiers the
account catalog has not listed yet, not for guessed names. Removed from the Codex fallback
list and template chain, the context/900k tables, the effort ladder prefixes, the aux-client
family list, and the Nous/OpenRouter static catalogs; catalog JSON regenerated. A contract
test pins the synthesized GPT-6 set to the published tiers.
2026-09-23 00:44:33 -07:00
teknium1
f5e6de6f4a chore: map otstructures@gmail.com to Lokee86 for attribution 2026-09-23 00:29:30 -07:00
teknium1
8fc679b4e8 perf(state): scan only the carried rows when every carried dict has a row id
The common micro-compaction pass carries dicts that all hold a matching
_row_id, yet _resolve_carried_row_ids re-read and re-derived identity for
every active row in the session inside the write transaction, O(active
rows) per turn on long sessions. Narrow the identity query to the carried
ids in that case and keep the full active-row scan for the fallbacks
(missing ids, or a stale id whose stored identity no longer matches), so
resolution semantics are unchanged.
2026-09-23 00:29:30 -07:00
Brian Fernstrom
91df54184d fix(state): preserve summarized tool rows in micro-compaction
Micro-compaction carries a non-contiguous prefix and suffix around its summary marker. Using tail_count=len(result)-1 incorrectly marked summarized assistant/tool rows as rewind-only active=0, compacted=0.

Pass the exact unchanged carried messages instead and resolve their durable originals transactionally by row id or unique identity+timestamp, preserving summarized rows as compacted history.

Fixes #118481
2026-09-23 00:29:30 -07:00
alt-glitch
c07501ec41 fix: two plugins installed together both go live with their MCP tools
The onboarding install card installs its rows concurrently, and each install
runs load_and_go_live: a forced plugin rediscovery, then a read of the plugin's
MCP server configs and skills, then an MCP connect. A forced rediscovery
unloads every plugin first (PluginManager.unload clears _portable_mcp_servers,
_plugin_skills and each server's liveness declaration) and only then loads them
again. When NVIDIA App and NVIDIA Broadcast finished installing together, the
Broadcast pass unloaded the manager while the NVIDIA App go-live was reading
it: NVIDIA App went live with no MCP servers and no skill (the card row said
"Installed" with no tool count, and the build chat had no NVIDIA App tools
until the app restarted).

load_and_go_live now runs one at a time in the process (_GO_LIVE_LOCK), so a
second install's rediscovery cannot run while the first reads or connects.
The connect is inside that lock because it reads the server's liveness
declaration (tools/mcp_tool_transport.py::_live_endpoint), which a forced pass
also clears. The reads share the manager's discovery lock with the pass that
produced them, for forced passes that do not go through load_and_go_live, and
connect_plugin_mcp takes the server configs read there instead of reading the
manager again. The MCP connect stays outside the discovery lock, so chats that
call discover_plugins() are not held for the length of a connect.
2026-09-23 12:45:32 +05:30
alt-glitch
5f47c35d37 fix: a connector row says "Waiting for your browser…" only after the user opens its link
The connect-first handoff (D85) has the build chat call manage_connections before
anything else, and the backend watcher mints each app's link on its first pass. That
moves the row to `initiated`, which the card drew as the spinner plus "Waiting for
your browser…" although nobody had clicked Connect and no browser had opened. Try
again had the same gap: the fresh link is minted, not opened.

The card now remembers which rows the user opened from it. A minted link the user has
not opened shows as not connected with the Connect button; clicking Connect opens it
and shows the waiting cue; Try again clears it until the new link is opened.
2026-09-23 11:54:42 +05:30
alt-glitch
72b58ef019 fix: tool_search tells the model to send keyword queries, not questions 2026-09-23 11:54:30 +05:30
alt-glitch
db0a15871f plugin-catalog: nvidia-app + nvidia-broadcast pin -> 65bb8d8 (skills let Hermes own the connection)
hermes-nvidia#2 rewrites both skills' connection guidance for Hermes: the
plugin connects the server; when the tools are missing the model relays
Hermes's unavailable sentence and stops instead of registering the server
itself, probing ports or reading server.json. Tool contracts unchanged.
2026-09-23 11:48:04 +05:30
alt-glitch
e458619f23 fix: the first build is named and seeded from the welcome chat's stored reply, not its streamed copy
On Windows (run 3, 2026-09-23) the renderer's streamed copy of the setup
reply repeated its own chunks. The handoff card read its task and brief from
that copy, so the build session was titled "Set up mySet up my games an…" and
opened with "Set up Sid up Sid's PC's PC for gaming for gaming…", cut at 240
chars. The backend's stored reply held the clean directive.

The card now reads the handoff directive from session.history (the persisted
reply) and uses those values; an unreachable history falls back to the
rendered attrs. It also waits for the whole reply to stop running: `locked`
tracks only the text part, which a later part settles mid-reply.
2026-09-23 11:46:41 +05:30
alt-glitch
d275e422dc fix: plugins installed a second apart both keep their install record
Each install read .install-metadata.json before its clone and wrote that
snapshot back after it, so the Desktop install card's second row erased the
first row's record (nvidia-app then showed source 'user'). Every writer now
re-reads the sidecar and changes only its own plugin's record under a
cross-process file lock; install rollback no longer rewrites a stale snapshot.
2026-09-23 11:45:29 +05:30
alt-glitch
dbe2f6964d fix: installing or connecting one MCP server no longer strips other servers' tools from the profile
connect_plugin_mcp and the connector flow call register_mcp_servers with only the
server they connect. The scope reconcile treated that dict as the profile's whole
config, so every other server in the profile looked removed and lost its overlay
while its connection stayed up: after onboarding installed nvidia-app then
nvidia-broadcast, the Plugins tab said nvidia-app was connected but no chat could
see its 12 tools. A name the caller omits is now judged against the profile's own
MCP config.
2026-09-23 11:45:12 +05:30
hermes-seaeye[bot]
c0d7294769 fmt(js): npm run fix on merge (#119817)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-23 04:02:02 +00:00
alt-glitch
6b9214b08e fix: the build session's runbook names each installed plugin's skill by its loadable name
Live run: the first default turn called skill_view("blender") and got
"Failed to load skill: blender". A plugin skill registers under its
qualified name (`agent-plugin-<key>:<skill>`), and the runbook carried only
the catalog name, so the model guessed the short one. The install card's row
already reports the qualified name; it now rides the settled outcome into
the runbook, which tells the build agent to load it by that exact name.
2026-09-23 09:26:07 +05:30
alt-glitch
d41f079e4e feat: Blender and NVIDIA each get a recommended first task
Sid's ruling after walking the flow: both plugin families carry a pill.
"Help me make something in Blender" is offered everywhere (the catalog runs
Blender on all three platforms). "Set up my games and streaming" (NVIDIA App
+ Broadcast) is offered on a Windows PC with an NVIDIA GPU, where it leads,
since those plugins are Windows-only. Each task runs the install beat for
its own plugins.
2026-09-23 09:26:07 +05:30
alt-glitch
d06ada6b0b fix: the connectors card paints its rows at once and keeps them across remounts
Live run: the card showed no chips for 3 to 5 seconds, drew them, lost them,
and drew them again. connectors.list takes 6.7 to 8 s on this Mac, and the
catalog read another 6.7 s cold. The hook read once per MOUNT, and the card
remounts on every transcript rebuild (each hidden submit, each turn end), so
each remount threw the rows away and waited out another round trip.

Both reads now live in the query cache keyed by the guide session, and the
kickoff prefetches them when the guide session opens (created or adopted),
two turns before the card needs them. A remount paints the cached rows.
2026-09-23 09:26:07 +05:30
alt-glitch
8f5ff1cd29 fix: curated catalog fields a published doc does not carry come from the checkout
Live run: the onboarding card listed no plugins. The published
plugin-catalog.json is built from main and stamps generated_at at build
time, so it outranked the checkout that added `onboarding: true`, and every
entry arrived without the flag. The same happens on a bundle built before
the catalog change reaches the docs site.

At the same pin, `onboarding` and `title` now come from the checkout when
the doc lacks the key. A doc that carries the key decides (so a curator can
turn it off upstream), and a different pin still follows the newer-catalog
rule unchanged. The previous commit's widened tie-break is reverted.
2026-09-23 09:26:07 +05:30
alt-glitch
6ec517c475 feat: onboarding offers catalog plugins beside connectors and installs the picked ones before handoff
NS-960. One card, one group ("connectors"): the curated catalog plugins
this OS runs lead the hosted connectors (D1, D4). A plugin whose app is
absent is greyed with the reason and stays pickable (D5). Picks land in
answers.plugins and ride the existing [setup] note to the guide.

The guide's runbook gains the install beat as the last thing before the
handoff card (D2, D3): narrow the picks to what the chosen task needs, then
ONE manage_catalog install call. A new suggested first task sits beside the
existing ones and installs its plugins through that same beat: "Set up my
games and streaming" (NVIDIA App + Broadcast) on a Windows PC with an NVIDIA
GPU, "Help me make something in Blender" everywhere else.

When the guide's install card settles, each plugin row's outcome
(installed / failed / skipped, whatever the user did) is written into the
answers (D6). The build session's runbook names what is ready, what was
offered and not installed, and what was picked but not offered, and tells
the build agent never to install. profiles.remember_onboarding records the
picked plugin names in the default profile's memory, next to the connectors.
2026-09-23 09:26:07 +05:30
alt-glitch
5c215ffa21 feat: catalog marks onboarding plugins; catalog rows carry the app's presence
The onboarding card needs to list catalog plugins beside the hosted
connectors (NS-960 D1, D4) and grey a plugin whose app is absent (D5).
The catalog had no curated flag, and the manage_catalog row left
app_state empty.

- `onboarding: true` and `title` on catalog entries (loader, validator,
  docs); set on blender, nvidia-app and nvidia-broadcast.
- hermes_cli/plugin_catalog_presence.py reads the plugin.json at the
  catalog's pinned commit once per pin and judges its app declaration with
  the hermes_platform resolver the installer and the Plugins-tab pill use.
  No declaration or an unreadable one is `unknown`, never `present`.
- `plugins.manage action=onboarding` lists the curated entries this OS
  runs (platform mismatch is the only exclusion) with app_state and the
  sentence the card greys the row with.
- manage_catalog plugin rows now carry app_state and the catalog title.
- A live entry that differs from the in-tree entry at the same pin (new
  metadata) now follows the same newer-catalog rule as a new pin, so a
  checkout that adds `onboarding` is not masked by a published doc that
  predates it.
2026-09-23 09:26:07 +05:30
teknium1
9fe737aef2 gateway: frame gateway.standalone as a TEMPORARY compatibility shim, not a kept topology
Multiplex-only is the direction. This key exists so fleets that lost per-profile gateways in
the switch keep working while the remaining gaps (per-profile stop/restart, WhatsApp
bridge/relay on secondaries, dashboard scoping) are closed, and it goes away once they are.
Every surface that names the key now says so through one shared notice
(STANDALONE_DEPRECATION_NOTICE): the install/start refusal hint, per-profile and default
`gateway status`, the migrate plan, and the host boot log (WARNING, not INFO). The user
guide carries a deprecation admonition where the key is introduced and no longer describes
it as opting out "for good".

Also: the Windows cold-start test fixture stubbed profiles_to_serve(multiplex) without the
new include_standalone kwarg (the PR's one red on CI).
2026-09-22 19:55:02 -07:00
Victor Kyriazakos
a29cc55e3b docs(gateway): document gateway.standalone, the per-profile opt-out of the host multiplexer
Flag list entry, the opt-out path in "No new per-profile gateways", what the
host does on rescan (<=30s, no restart), what happens when the key is removed
while the profile's own gateway runs, and that a standalone profile's cron,
webhook ingress and kanban notifications run only under its own gateway.
2026-09-22 19:55:02 -07:00
Victor Kyriazakos
0238c9d740 feat(gateway): gateway.standalone opts a named profile out of the host multiplexer
A named profile that authors `gateway.standalone: true` in its own config.yaml
runs its own gateway again, the pre-multiplex topology, while the default
gateway keeps serving every other profile. Topology becomes something the
operator authors per profile instead of something the box infers from boot
state, which is what a fleet running per-profile gateways lost when
`gateway.multiplex_profiles: false` was retired.

Changed
- hermes_cli/profiles.py: `profile_is_standalone(home)` reads the profile's
  own config.yaml (memo by file signature, tolerant of malformed yaml, always
  False for the default profile with one warning). `profiles_to_serve()`
  excludes standalone profiles; roster callers that mean "every installed
  profile" (plugin deps, Windows update, launch policy, dashboard listing and
  topology) pass `include_standalone=True`.
- gateway/host_attach.py: `standalone_attach_decision` starts a standalone
  profile's gateway beside the host multiplexer once every live gateway
  confirms it does not serve that profile; refuses with a rescan message while
  one still does. Used by the initial attach check and the lock-losing race.
- hermes_cli/gateway_multiplex_mode.py: a standalone launcher never becomes
  the host multiplexer (`STANDALONE_PROFILE_REASON`), including callers that
  supply an explicit GatewayConfig.
- hermes_cli/gateway.py, web_server_gateway.py: `hermes -p X gateway
  install/start/run` proceeds without --force for a standalone profile; the
  refusal text for other profiles points at the opt-out; status shows
  "standalone (gateway.standalone: true)" and the default lists skipped
  profiles.
- gateway/run_profile_reconcile.py: the host does not re-adopt a profile whose
  own gateway is live (removing the key while it runs no longer double-binds).
- hermes_cli/gateway_migrate.py: standalone profiles are neither blocker nor
  fold target; the plan lists them as "standalone by config".
- gateway/run.py: one INFO line per standalone profile at host boot.

Tests: two-home E2E through real loaders and resolve_multiplex_mode, decide()
with fake host records for both arms, lock-losing branch, reconcile guard,
migrate plan, refusal predicate both ways, topology, memo and malformed-yaml
contracts. All red on base.
2026-09-22 19:55:02 -07:00
alt-glitch
550d74c62f fix: manage_catalog documented under its own setup toolset; post-hook ownership contract exercises it
tools-reference.md listed manage_catalog under the connections section, which the
reference-docs contract resolves against the connections toolset. The agent-runtime
post-hook ownership test enumerates every tool in AGENT_RUNTIME_POST_HOOK_TOOL_NAMES;
manage_catalog now runs through both executor paths there.
2026-09-23 08:04:33 +05:30
alt-glitch
829a881a52 fix: catalog skill rows read hub metadata without a download; an already-installed skill says so
The row's name and description came from inspect_skill, which fetches the whole bundle.
The first hub source's inspect() answers with metadata alone. A skill already in the lock
file (force off) is left as it is and the connected row's detail says so, instead of
settling as if it had just been installed.
2026-09-23 08:04:33 +05:30
alt-glitch
31e59b9441 feat: setup agent can search the catalog and install plugins and skills through the approval card
The setup profile's `setup` toolset was empty. It now carries one tool, manage_catalog:

- search: catalog plugins (the Plugins tab's live catalog resolver) and hub skills, with
  whether each is already installed in the default profile. Read-only.
- install: opens the same connection operation manage_connections opens, with rows of kind
  plugin / skill. Nothing installs until the user approves a row. An approved row installs
  into `default` (or the profile the Advanced modal named) through dashboard_install_plugin /
  the hub's headless install, so the catalog pin, kill list, security scan and live
  activation (#119644) are the host's. The row settles with the live MCP tool names and the
  plugin's skill.

The model sends catalog ids and an action only; every other key is refused before anything
runs. An unknown id or a plugin this OS cannot run is drawn failed with the installer's own
text. Anywhere a catalog card cannot be drawn (TUI, CLI, messaging, registry dispatch) the
result is the `hermes plugins install` / `hermes skills install` pointer.

- contract: plugin/skill targets follow the MCP transitions.
- run.apply_answer / reissue route a card answer to the module that owns the operation.
- tool_search: `setup` joins the direct-surface toolsets, so the guide's one tool is never
  deferred behind tool_search.
- docs: tools reference, toolsets reference, plugin catalog page.

Linear NS-964.
2026-09-23 08:04:33 +05:30