Commit Graph

4639 Commits

Author SHA1 Message Date
Teknium
71704ffb49 refactor: move Spotify out of core into the spotify catalog plugin
The spotify toolset (plugins/spotify) and its loopback-PKCE login (hermes_cli/auth_spotify.py)
now live in NousResearch/hermes-spotify, installed from the plugin catalog. Same pattern as the
Home Assistant move (fb9fc8a3dd): a LEFT_CORE row installs the plugin for every home that was
using it (a Spotify login in auth.json, or the spotify toolset in platform_toolsets) on
`hermes update` / first start; the toolset name, tool names, schemas, env vars and stored login
(providers.spotify) are unchanged, so nobody re-authenticates.

The one user-visible change is the login command: plugins cannot extend `hermes auth`, so the
plugin registers `hermes spotify login|status|logout`. LeftCoreFeature gains a `cli` field and
moved_command_hint() turns `hermes auth [status|logout] spotify`, and `hermes spotify` while the
plugin is not loaded, into a line naming the new command and the install/enable step for this
home. The dashboard's "needs login" hint names the new command too.

Core drops the Spotify auth constants/dispatch, the CONFIGURABLE_TOOLSETS row and post-setup
step, the core `spotify` entries in the shared-metrics toolset enums, its tests, and the docs
(en + zh-Hans) now point at `hermes plugins install spotify` / `hermes spotify login`.
2026-10-08 19:00:45 -07:00
Teknium
7e17bab684 docs(gateway): a service definition belongs to the home it pins 2026-10-08 17:06:02 -07:00
teknium1
05df0382a5 fix(plugins): re-enabling a plugin restores its toolset in a saved platform_toolsets list
The Desktop/TUI/dashboard toggle edited the saved list before the plugin
loaded, so a backend that scanned while the plugin was off found no toolset
key and left the toolset out: the plugin showed enabled with its tools off.
Enable now edits the list after activation; disable resolves the key before
the disable is written. hermes plugins enable/disable make the same edit.
Only saved lists are touched; a fresh config still gets no list.
2026-10-08 15:15:47 -07:00
Maciej Lisiewski
d76cce4f11 ci: apply all automatic linter fixes available 2026-10-08 17:22:54 +02:00
teknium1
8f463316fd feat(plugins): one name resolver for every verb; one switch turns a unified package on
- hermes_cli: _find_plugin_entry is the single resolver (key / manifest name / unique leaf /
  catalog name via the installer-owned record). enable/disable/show/capabilities/update/remove,
  the dashboard and plugins.manage settings all go through it, so the catalog name you
  installed with keeps working (17 catalog entries install under another manifest name).
  _catalog_install_on_disk reuses the same catalog lookup.
- Desktop hub: join agent rows to desktop halves by package folder (what Electron stamps),
  not manifest name, so a self-cloned package is one row instead of two.
- Desktop: turning a unified package on (install dialog 'Enable after install' or the Agent
  switch) also turns its opt-in desktop half on, unless the user switched it off. Off stays
  per half. Capability consent is untouched.
- docs: plugin-catalog page.
2026-10-08 04:12:02 -07:00
teknium1
dacb06ea93 feat(plugins): show on_session_finalize return messages to the user
A plugin's on_session_finalize hook can now return a str or {"message": str}
and Hermes shows it at the session boundary without starting a turn or
rewriting the model reply:

- CLI /new: printed before the new session; CLI quit: printed above the
  exit summary; one-shot -q/-z: printed to stderr (stdout stays the answer)
- TUI: session.close returns the messages; /new shows them as system lines
- Gateway /new and shutdown: sent to the owning chat via the adapter, while
  adapters are still connected

on_session_end stays observer-only: it fires after every turn, not at a
session boundary. Gateway finalize dispatch moves to
gateway/run_shutdown_session_end.py.
2026-10-08 04:11:24 -07:00
teknium1
cc1500a071 fix(desktop): a linked plugin checkout drives its desktop half; doctor flags a stale copy
The Desktop loads a unified package's desktop half from the copy in
desktop-plugins/<name>/, not from the package. Editing the source shipped
nothing and nothing said so:

- a package symlinked into plugins/ (the in-place dev layout the Python
  loader already follows) was never copied at all — Dirent.isDirectory()
  is false for a symlink;
- an installed package edited in place stayed stale until Rescan/restart.

Electron now follows symlinked package dirs, marks their copy linked, and
re-syncs it on any byte difference. The renderer watches a linked copy's
source plugin.js and, on save, runs the reconcile and hot-reloads. Catalog
and git installs keep the mtime rule. `hermes plugins doctor` warns when
the copy differs from the source and says how to refresh it.
2026-10-08 04:07:30 -07:00
Teknium
e760a20d89 test(approvals): fold the unattended-presence cases into two invariants; document the api_server bridge exception
Replaces the six TestUnattendedPlatformExecAskLeak cases in test_approval.py (past its
size cap) with two invariants in a sibling file, both red on main:
- webhook / msgraph_webhook deny from approvals.unattended_mode even with the
  TurnRunner-registered notifier, and never reach the human gate;
- api_server denies without a notifier and keeps the ask path once a client bridge
  (/v1/runs, streaming chat) registers one.

security.md: unattended_mode now names the api_server exception.

Root cause first identified in #100554 (@fangliquanflq); webhook leg from #134821.
2026-10-08 03:50:22 -07:00
teknium1
82b9bfedd6 fix(plugins): enabling a plugin from Desktop/TUI/dashboard no longer strips the core tools
With no saved platform_toolsets, _toggle_plugin_toolset seeded
platform_toolsets.cli: [<plugin_ts>]. A list naming only plugin keys
replaces the hermes-cli composite, so the model lost every built-in tool
(26 -> 1, no terminal). The seed is dropped: an unknown plugin toolset is
already on by default when no list is saved.

plugins remove now clears the plugin's toolset from saved lists even when
plugin.yaml has no provides_tools (the toggle already no-ops without a
toolset); before, hermes config check warned about an unknown toolset.

Docs: say what provides_tools actually drives.
2026-10-08 03:40:04 -07:00
witcheer
dde8800ed9 docs(plugins): list the built-in author tools at the top of the plugin guide 2026-10-08 01:02:23 -07:00
ethernet
cfb9ae4f86 Merge pull request #134905 from NousResearch/fix/release-seed-0215
Patch releases continue at 0.21.6 instead of re-cutting 0.21.5
2026-10-08 00:09:50 -04:00
Teknium
3a43872105 docs(security): /yolo survives a TUI/Desktop resume too 2026-10-07 21:00:20 -07:00
Teknium
65c6fb521d fix(release): continue the version line after the legacy v0.21.5 release
The pipeline seeded its version line at 0.21.4, but the legacy CalVer line
already published "Hermes Agent v0.21.5" as v2026.9.24. A first `--bump patch`
cut therefore derived 0.21.5 again: installs already reporting 0.21.5 would
compare equal and never see the new build as an update, and the release page
would carry two v0.21.5 entries.

Seed at 0.21.5 so the first pipeline patch is 0.21.6 and the first minor stays
0.22.0. Entrypoint fixtures now pin their own published head instead of
tracking the production seed.
2026-10-07 20:58:19 -07:00
ethernet
bc2e4d3773 Merge pull request #134209 from NousResearch/sid/onboarding
feat(onboarding): first-run setup chat
2026-10-07 19:17:15 -04:00
Teknium
889eddcd86 fix(gateway): /yolo survives a gateway restart
/yolo on a messaging platform only flipped tools.approval's in-memory
set, so any gateway restart (hermes update, crash, systemd restart,
Desktop reopen) silently put the chat back on manual approvals while
the user still believed YOLO was on.

The flag now lives on the chat's routing entry (SessionEntry.yolo,
persisted in gateway_routing like the /model override). /yolo writes it
before flipping the live flag, materializing the entry for a
first-message toggle; every turn re-arms the live flag from the entry it
already resolved; switch_session carries it across compression-tip and
binding repoints; the conversation-boundary security funnel (/new,
/resume, /branch, auto-reset) clears the persisted copy with the
in-memory one so a restart can never revive a bypass the user dropped.

Also corrects the security docs, which still claimed /yolo sets
HERMES_YOLO_MODE.
2026-10-07 16:15:35 -07:00
Austin Pickett
cc11102273 docs(web): note the dashboard build's resource caps (#63338) 2026-10-07 17:13:11 -04:00
Mark S.
3cabfad57e feat(models): add anthropic/claude-haiku-5.5 to OpenRouter and Nous Portal catalogs (#134759) 2026-10-07 17:12:40 -04:00
Austin Pickett
3adc457de5 Merge remote-tracking branch 'origin/main' into sid/onboarding
Conflicts, all in apps/desktop/src/i18n:
- de/en/es/fr/zh.ts, types.ts: import blocks only. Main added the
  *_projects section split, this branch the *_onboarding one; kept both.
- en_shared_metrics, zh_shared_metrics, ar_common, zh-hant (usage-stats
  consent copy): kept this branch's rewritten copy. Main's only change on
  its side since the merge base was a prettier reflow of
  collectedReliability (2943ee6f19), which the branch's rewrite replaces;
  types_shared_metrics on HEAD already matches the branch's keys.

Follow-through: main's Undo-hide toast (sidebar.projects.hiddenFromSidebar /
undoHide) arrived without ar or zh-hant strings, which this branch's
overlay-completeness test rejects. Added both to ar_chrome / zh-hant_chrome
and regenerated locales/_keys.desktop.json.
2026-10-07 16:16:23 -04:00
Teknium
3ca97d5e59 fix(pm): orphan reclaim runs on every prune pass and keeps checkouts it cannot see
Follow-up to the review of this PR:

- The startup pruner returned early (no .worktrees/, or no tree past 24h) before the
  reclaim ran, so a tree removed by hand kept its state until some other tree aged out.
  _prune_stale_worktrees now runs the reclaim in a finally around the prune phases.
- A missing recorded path is not proof of deletion: a container sharing the data root
  (-v ~/.hermes:/opt/data) cannot see the host's checkout, and gc there deleted the
  host's live dependency environment. A state is an orphan only when its checkout sat
  under the data root that owns installs/, or directly in a .worktrees/ dir that still
  exists.
- PM runtime generation leases (pm-runtime/generations/*/.leases) now count as held,
  so a worker still running from a deleted checkout's runtime keeps it.
2026-10-07 08:47:47 -07:00
Teknium
ef4b046808 fix(pm): reclaim dependency state of deleted checkouts (pm gc + startup worktree prune)
Every checkout that boots Hermes commits its own dependency state under
<home>/installs/<sha256(path)[:16]>/ (venv, test venv, PM runtime; ~200 MB
each) and nothing removed it when the checkout went away. On a host that
runs `hermes -w` campaigns the directory held 499 entries / 92 GB, of which
402 (80 GB) belonged to worktrees and scratch clones that no longer exist.

The state dir already records its checkout in inputs/.project-root. A new
pm/install_states.py lists entries whose recorded checkout is gone and
removes them unless something could still be reading them (install lock
taken, or a generation lease held). `pm gc` runs it and reports the count;
the startup worktree pruner runs it after its own reaping so a removed
worktree's state follows the tree instead of outliving it.
2026-10-07 08:47:47 -07:00
alt-glitch
db02737cf3 Merge remote-tracking branch 'origin/main' into sid/onboarding 2026-10-07 20:37:24 +05:30
alt-glitch
2c8df45ee5 Merge remote-tracking branch 'origin/main' into sid/onboarding
# Conflicts:
#	agent/conversation_loop.py
#	tui_gateway/prompt_turn.py
2026-10-07 19:29:35 +05:30
John Paul Soliva
dc824c5f65 fix(auth): save a profile's first single-use login, and never print Added for an unsaved row
- Decide the profile claim when the row is written (the check from #101827)
  and drop the None sentinel from #103712. On current main load_pool() leaves
  a profile that borrows from an empty root at the default, so the sentinel
  alone never reached the claim path (its test stays red).
- add_entry() re-reads the store after writing. A missing row raises
  CredentialNotSavedError; `hermes auth add` exits with that message instead
  of printing "Added", and the dashboard pool endpoint returns it as a 400.
- Tests: the two contributor tests become one invariant over every
  SINGLE_USE_REFRESH_POOL_PROVIDERS provider (the row is readable, root
  auth.json is byte-identical), plus the CLI success-line contract.
- Docs: profiles page, EN and zh-Hans.

Fixes #103694

(cherry picked from commit 0e250a005a3353564642ce70f100d89174a583f2)
2026-10-07 09:59:11 -04:00
Teknium
ed2dd0b35c fix(cron): live-owner stale-claim reclaim measures silence, not run length
The #115692 reclaim released any running attempt whose claim was older than
max(3 x HERMES_CRON_TIMEOUT, script timeout, 2 h), even when the owner was
healthy and busy. Every cron job that legitimately ran longer than that was
marked `unknown` mid-run, its fire claim yanked from under it and the run
aborted with "lost its durable fire claim ownership" (two weekly jobs on one
install in a single day).

The run monitor now stamps `progress_at` on the execution row every 60 s
while the agent reports recent activity, and the sweep measures the stale
bound from that stamp (claim age for legacy rows). A wedged worker stops
stamping and is still reclaimed once the bound passes.

Shape: the stamper and the inactivity watchdog loop (which reads the same
idle signal) live in a new topical sibling cron/scheduler_liveness.py, so
cron/scheduler.py shrinks (4457 -> 4436 lines) instead of growing.
2026-10-07 06:29:09 -07:00
Teknium
b8f007cc4f docs(telemetry): how update dashboards count a partial run 2026-10-07 06:15:58 -07:00
Teknium
476e0f1138 fix(telemetry): pre-receipt exits keep their initiator; index.lock permission errors; bare-interpreter updates park
- record_stop_without_receipt reads the update marker raw and tags initiator=desktop only when the
  marker names this run's hand-off (delegate line = us, or HERMES_UPDATE_HANDOFF_PID / our parent),
  so a Desktop-started early refusal reads kind=desktop and a CLI run refused by someone else's
  update stays cli.
- _move_checkout_to classifies its git error with the shared classifier (git_output_stop_class):
  index.lock 'File exists' -> git_index_locked, 'Permission denied'/EACCES -> permission_denied
  (now a stop class), else checkout_move_failed.
- _publish_shared_metrics: an interpreter that cannot import the config reader (the -I -S bootstrap
  that finalizes a dependency-preparation failure) never emits; it parks the bounded receipt in
  pending_updates unless it can tell collection is off (loaded config says off, or no config.yaml),
  and the next normal start applies the existing gate (report_pending_updates / begin_process purge).
2026-10-07 06:15:58 -07:00
Teknium
b49e2ff2b9 fix(telemetry): review findings on pre-apply stop reasons
- The parked copy keeps a stop reason's label only when it is an exception type name (plus its
  errno token) or one of Hermes' two fixed phrases; any other text before a colon reads '-'
  (a free phrase like 'my project name:' used to survive).
- _exit_after_failed_branch_switch's branch_missing/checkout_move_failed probe cannot raise, so
  the exit it names is unchanged even when git refuses.
- hermes update --plan/--check/--list-venv-holders on a managed install records no update run.
- Docs/contract say 'before the apply stage mark' and name the tokens that fire after git moved
  and restored the tree; tests drive git_error_stop_class and _zip_stop_class directly.
2026-10-07 06:15:58 -07:00
Teknium
b5a8fba1b4 test(telemetry): pre-apply exit classes, parked parity, receipt-less refusals; docs + smoke
- test_shared_metrics_update_stop_reasons: every stop token reads as its own class in-process
  and through the parked copy (no free text kept), unknown tokens and post-apply tokens fall
  back, a committed run that only owes parked changes is partial; the lock and Git-operation
  refusals each record one row and leave the holder's latest.json byte-identical.
- test_shared_metrics_install_failures: a PermissionError stop reason now reads its own class
  permission_denied (was os_error); ENOSPC reads disk_full; other OSErrors keep os_error.
- relay-shared-metrics.md: the new classes, partial outcome and parked fields.
- relay smoke: the failed update receipt names its fetch failure and the row asserts fetch_failed.
2026-10-07 06:15:58 -07:00
Teknium
74943bb889 docs: stop presenting Desktop Light as a shipped download
The installation page said "Light is a remote-only build variant" next to
the download instructions, which read as if a remote-only Desktop client
can be downloaded. Light exists only as a build target with no release
leg, and there are no plans to publish it. Replace the sentence with how
remote use actually works today (install a package, connect from Settings
-> Gateways), drop "Light packages" from the uninstall note, and mark the
variant as unpublished in the desktop README.
2026-10-07 05:14:58 -07:00
Teknium
1e347ccf2a feat(plugins): a portable package can ask Hermes to gate its MCP server
Agent Plugins v1 has no trust field, and Hermes's portable mcp.json reader
accepts only type/url/headers for a remote server, so a package server always
ran at the default trust: full. A package whose tools trade, spend or send
(senpi resolving trade approvals on a user's Hyperliquid account) could only
ask the model to check with the user; Hermes itself never prompted.

plugin.json's Hermes extension now accepts
  extensions.com.nousresearch.hermes.servers.<name>.trust: untrusted
which is applied to that mcp.json server exactly like a config.yaml
'trust: untrusted': every write-capable call asks first and fails closed
when unattended. 'full' (the default) is accepted and changes nothing, any
other value refuses the package, so a package can narrow access but never
widen it, and a config.yaml server with the same name still replaces the
package's entry. 'hermes plugins validate' lists the gated servers.
2026-10-07 04:51:23 -07:00
Teknium
7664cc21ad fix(update): the POSIX gateway pause never blocks an update
A discovery or durable-record failure refused the whole update at arm_commit_point (exit 1),
so a host where systemctl cannot reach a user bus (SSH without lingering, containers) could no
longer update at all. The pause is protection, not a gate: any failure restarts whatever it
stopped and the update continues the way POSIX always updated (gateways run through the swap,
restarted after the commit). A failed rollback stays owed and restarts after the deps sync; a
successful one clears the restarted set so the fleet restart refreshes those gateways.

Caught by six existing completion/branch-switch tests whose fake subprocess layer makes
discovery raise; they pass unchanged.
2026-10-07 04:39:00 -07:00
Teknium
082ec546aa fix(update): Windows keeps resuming after the fleet restart, never twice
The early before_build resume ran for Windows sets too. The fleet restart skips only what the
POSIX pause restarted (already_restarted), so a Windows gateway resumed before the build was
then drained and restarted a second time by the fleet pass: one extra outage per update that
main never had. The early resume is now POSIX-only and goes through the main facade like every
other resume call; Windows resumes where it always did. Docs say so.
2026-10-07 04:39:00 -07:00
Teknium
d16d2c5c41 test(update): hostile cells + docs for the POSIX pre-swap pause; leave unprivileged system units running 2026-10-07 04:39:00 -07:00
Teknium
e3d2e57c9e fix(skills): background review can improve every skill, not just curator-managed ones
The self-improvement review fork refused every edit/patch/write_file/remove_file on a skill
without `created_by: "agent"`, so lessons about hand-written, URL-installed, foreground-created,
bundled, hub, external or pinned skills were dropped with only a WARNING line (77% of refused
review writes on the #134289 reporter's install). The prompts then steered the fork to a
different umbrella or a new near-duplicate skill.

Ownership now gates only delete/archive: the review may improve any skill it learns from (still
read-before-write, ledgered, and staged when skills.write_approval is on), while autonomous
archival stays limited to curator-managed, unpinned, non-bundled/hub/external skills. The review
prompts drop the protected-skill lists and say never to delete.

Fixes #134289
2026-10-07 02:27:40 -07:00
Aniruddha Adak
88704223d2 fix(memory): scrub secrets on every memory-provider hand-off (#115104)
MemoryManager forwarded turns, transcripts and tool output to external
memory providers without the egress scrub, so a key echoed into tool
output was archived verbatim in the provider's store.

Every fan-out now goes through redact_for_egress (the chat/cron delivery
scrub; holds with security.redact_secrets off, fails closed): sync_all
(turn strings + messages), prefetch_all / queue_prefetch_all,
on_turn_start, on_session_end, on_pre_compress (both transcripts),
on_memory_write (content + metadata), on_delegation and provider
handle_tool_call args. Nested dicts/lists (tool-call arguments,
multimodal parts) are walked; data: URLs pass; the live transcript is
never mutated. A value masked anywhere in one hand-off is masked
everywhere in it, so a key the model repeats bare in prose is caught.

redact.py: the JSON-field rule now also matches inside JSON-encoded
strings (tool results wrap config dumps in escaped JSON), which is how
the reported opaque AQ.-prefixed key slipped past.

Co-authored-by: Teknium <teknium1@gmail.com>
2026-10-07 02:06:15 -07:00
Teknium
98a6dcf586 feat(voice): voice turns default to reasoning off, the lowest valid level where off is refused
auxiliary.voice_chat.reasoning_effort ships as "none", and it applies even when the slot is on
auto (the session's model answers the spoken turn with reasoning off; typed turns keep their
effort). Routes that cannot switch reasoning off get their weakest accepted level instead:
Responses ladders without "none" (gpt-6-astra, gpt-6.1-sol -> low), mandatory-thinking Claude,
and routes the catalog or an earlier 400 marked mandatory. A disable the voice model rejects at
runtime is remembered per route for the next voice turn, and the rejection flags never leak
between the voice route and the main one.

"Inherit" in the dashboard/Desktop writes an explicit "" so the slot default does not come back;
both resets restore the shipped default.
2026-10-07 01:35:11 -07:00
Teknium
6cc1efd8f9 feat(voice): auxiliary.voice_chat routes spoken voice turns to their own model
Voice replies had to come from the session's main model, so a heavy model made every spoken
answer slow (#83894). A new auxiliary slot, voice_chat, picks the model for voice turns:

- CLI/TUI voice mode, the Desktop voice conversation (prompt.submit voice_turn) and messaging
  voice notes mark the turn; agent/voice_turn_route.py binds the route after the prompt, row and
  preflight compaction were settled against the main model and undoes it before memory sync and
  background review, so the next typed turn and the review run on the main model.
- A turn larger than the voice model's compression threshold stays on the main model with a
  one-time notice: switching to voice never compacts the conversation.
- Usage lands in session_model_usage under task voice_chat; it never rewrites the session's
  recorded model/route, so resume keeps the model the user picked.
- Slot appears in Desktop Settings -> Models, the dashboard, and `hermes model`; GPT-Live
  delegations keep using the session model.
2026-10-07 01:35:11 -07:00
kshitijk4poor
77bf0d7f73 refactor(gateway): one drain predicate for the fire gate and the adapter
Lead fold from the independent review (quality + thermo + 2c + efficiency
lenses, all Low): the fire startup gate and
APIServerAdapter._gateway_is_draining now share runner_is_draining, so a
future drain flag cannot be added to one and missed by the other; the gate
returns early on no runner instead of looping on an unchanging guard. The
cold-adapter test closes its loop and joins its thread, the slow-verify
test keeps the same ordering at a fifth of the wall-clock, and the Chronos
contract doc lists the startup 503.
2026-10-07 13:43:19 +05:30
alt-glitch
17646902e1 Merge remote-tracking branch 'origin/main' into sid/onboarding 2026-10-07 13:28:50 +05:30
alt-glitch
9b2c2554de docs(desktop): say which app checks setup runs
The desktop guide said nothing else on the computer is read for setup, but building the setup cards checks whether Blender and the NVIDIA apps are installed, reads their versions and for some apps their runtime file. Greptile on #134209.
2026-10-07 12:58:11 +05:30
Teknium
05eecbcd97 chore(memory): remove the bundled openviking provider
OpenViking now ships from Volcengine's own repo through the plugin catalog
(plugin-catalog/openviking.yaml, pinned in #131267). Delete the in-tree copy
and its tests; memory_provider_migration already lists openviking in
_LEFT_CORE, so homes configured for it get the catalog plugin installed on
hermes update / first agent start.

The catalog pin starts openviking-server with the full Hermes environment
(the bundled copy filtered it). The docs now say so and point at
volcengine/OpenViking#5553; start openviking-server yourself to avoid it.
2026-10-06 23:13:01 -07:00
alt-glitch
7f8fa7843e refactor(onboarding): drop app discovery and the guide_* funnel steps
The desktop's old recommendation inputs were the only client of
GET /api/mcp/catalog?detect_apps=true and the only sender of the guide_*
onboarding steps; both went with the old ::onboarding chain.

- The catalog route loses detect_apps; mcp_app_detection.py and its two
  test files go. The manifest's suggest.applications validator moves into
  mcp_catalog.py, which still validates applications / requires_app /
  examples (documented in features/mcp.md).
- DESKTOP_ONBOARDING_STEPS and the v4 schema drop guide_connectors,
  guide_first_build, guide_layout and guide_look. v3 stays as published.
- developer-guide/onboarding-recommendations.md described that pipeline
  and is removed.
2026-10-07 11:38:21 +05:30
Teknium
d78d031f34 fix(vision): computer_use captures honour image_input_mode and catalog text-only models
The capture route only checked "does the provider carry tool-result
media", never `agent.image_input_mode` or a catalog verdict that the
model cannot see. deepseek-v3.2 on OpenRouter therefore went native, the
agent backstop swapped the screenshot for a "switch to a vision-capable
model" error, and the configured auxiliary vision describer never ran;
`image_input_mode: text` was ignored for screenshots too.

`tools.vision_tools._native_tool_result_images(provider, model, cfg)` is
now the one gate for every tool that hands the model pixels
(vision_analyze, browser screenshots, computer_use captures, MCP images):
image routing resolves to native AND the route accepts tool-result
images. The capture route is its negation, which drops three local
helpers that duplicated image_routing. Proxy aliases the catalog does
not know still go native under `image_input_mode: native` (#115248's
case); `auto` sends them to aux, matching vision_analyze.

Tests: test_computer_use_vision_routing.py is rewritten to drive the
real gate (the old cases patched the removed helpers). Changed
assertions: "whitelisted provider + catalog-unknown model" now requires
image_input_mode: native (it carried that cfg already); new cases pin
catalog text-only -> aux and image_input_mode: text -> aux.
2026-10-06 22:54:13 -07:00
Teknium
6da034a60d fix(mcp): a vision model sees MCP ImageContent instead of only its MEDIA: path
An MCP tool's ImageContent was cached and the model got back only a
`MEDIA:/path` line, so a vision main model could never read the image
(a raw cua-driver MCP server could not see the screen at all).

The registry handler now returns the `_multimodal` envelope when the
active route takes images inside tool results, decided by the same gate
vision_analyze and browser screenshots use (`agent.image_input_mode`, an
explicit `auxiliary.vision` backend, catalog vision, provider support).
The image is re-encoded from the sniffed cache file, normalized to a
provider-accepted format and sized to `vision.embed_target_bytes` /
1568 px (it is re-sent every later turn), at most 4 per result; the text
half keeps the MEDIA: paths. Paths come from this call's own cache
writes, never parsed out of server text. The resize runs on the tool
thread, off the shared MCP loop. Plugins' `ctx.call_mcp` keeps its
string contract (native_images is set only on the registry handler).

Builds on #131374 by @sarahwu185, which reused the computer_use capture
gate (ignores image_input_mode, native for catalog text-only models) and
inlined the server's raw base64 unvalidated and unresized.
2026-10-06 22:54:13 -07:00
tank321
9de894da21 fix(stt): local turbo model is selectable in every STT picker (#79629)
faster-whisper accepts `turbo` and the config docs listed it, but the CLI
picker, dashboard and Desktop settings stopped at large-v3, so the only way to
use it was hand-editing config.yaml. All three now read it from the shared
STT_MODEL_CATALOG, along with the install hint and the cloud-model warning.

Salvage of #79671 by @tank321, rebased onto the unified catalog from #133825.
2026-10-06 22:01:38 -07:00
kshitijk4poor
d85d23cfb7 docs(cron): store gauges are host-wide; notices use the owning profile's language 2026-10-07 10:20:18 +05:30
kshitijk4poor
33530d1207 fix(gateway): defer the cron store "recovered" notice instead of dropping a re-entry
Gate r4 (2c + quality, both probed): the r3 hysteresis dropped an "unwritable"
transition that came within an hour of the last notice. note_unwritable reports
entry only once, so for unwritable -> recovered -> unwritable the home channel's
last notice said "writable again" while the store stayed down, possibly for days.

Rate-limit the recovery instead. "unwritable" is sent only for a store whose
outage was not announced yet; "recovered" waits NOTICE_REPEAT_SECONDS on the
gateway loop and is sent only if the store is still writable then; a re-entry
meanwhile cancels the pending recovery and sends nothing. The last notice always
matches the store's real state, and a flapping disk posts at most one pair per
window. All bookkeeping now runs on the loop (call_soon_threadsafe), so the
ticker thread no longer needs a lock, and the notice coroutine is created
there too: a transition after the loop closed raises before any coroutine
exists (logged by store_health._notify), instead of leaking an un-awaited one
(gate r4 reuse item, loop-side variant).
2026-10-07 10:20:18 +05:30
kshitijk4poor
6eea783601 docs(cron): note that a gateway restart mid-outage drops the one-shot carve-out
Gate finding C12 r3 #5 (2c): the degraded state lives in gateway memory, so a
one-shot that came due during an outage only fires on recovery if the
gateway did not restart in between. After a restart the normal grace window
applies and it can expire. Say so in the recovery sentence rather than
seeding "since" from jobs.json's mtime, which would revive one-shots that
were really missed.
2026-10-07 10:20:18 +05:30
kshitijk4poor
fc9473ccc1 docs(cron): document the unwritable-store headline, notices and fix steps
The troubleshooting section beside `hermes cron status` now covers what users see when the cron
store can't be written: the status headline, the `cron list` banner, the doctor check, the
home-channel enter and recover notices, and the metrics. It also gives the fix (free disk space,
remount read-write, fix permissions) and says that each due job fires once on recovery.
2026-10-07 10:20:18 +05:30
emozilla
43c1271009 fix(cli): the CLI's first message reaches the model as typed
Main sends the first-contact note (the profile-build offer or the plain
intro) from the messaging gateway and the TUI/desktop backend only. This
branch added a third caller in the CLI chat turn, so the first `hermes chat`
message in a fresh home gained the profile-build offer and wrote
onboarding.seen.profile_build_offered to config.yaml. That includes one-shot
`chat -q` runs and kanban workers on new profiles.

The CLI caller goes; /initiate-setup stays a CLI command. The configuration
reference no longer lists the CLI among the surfaces that make the offer.

test_cli_first_message runs a real `hermes chat -q` in a fresh home against
the fake provider: it fails on 70f1ba1637 and passes on main and with this
change.
2026-10-06 23:25:40 -04:00