Commit Graph

738 Commits

Author SHA1 Message Date
Teknium
32a79afb27 Merge pull request #132361 from NousResearch/fix/update-commit-point
fix(update): make the git/ZIP swap a single crash-safe commit point
2026-10-06 11:11:39 -07:00
Teknium
e032fdbca1 refactor(telemetry): registry prefixes come from the hub adapters; update reasons use tables
- `skills_hub._registry_prefixes` builds the identifier prefixes from the adapters' own
  SOURCE_IDs and `SkillsShSource._ID_PREFIX_ALIASES`, case-insensitively, so `skills.sh/...`,
  `skils-sh/...` and `OFFICIAL/...` stop reading `unresolved`. A matched source is named by
  `source_id()`, the method the router and the contract test use; two test fakes gain it.
- `update_failure_class`: exception type -> class and fixed stop-reason prefix -> class are
  tables instead of an if-ladder. `Windows gateway recovery failed: ...`, the fixed prefix
  update_completion writes when the post-update gateway resume fails, now reads
  `restart_failed` instead of `other` (prefix only, never the message).
- `mcp.servers.add` records through `mcp_rpc_helpers.record_mcp_add`, dropping the 175-column
  noqa line that kept methods_tools.py at its cap (2011 lines now).
2026-10-06 11:10:43 -07:00
Teknium
d6813a5dd7 fix(telemetry): extension failure classifiers are inert; the contract rejects contradictory install rows
- `_publish_failure_class`, `probe_failure_class` and `exception_failure_class` read a raise
  site's `failure_class` with `inspect.getattr_static` (`tagged_failure_class`), and the MCP
  type walk falls back to `connect_failed`. A third-party exception whose `__getattr__` or
  property raised replaced the user's PluginOperationError with RuntimeError (they run at the
  raise site, collection on or off), and the metric row was dropped; it now reads `exception`.
- `counter_dimensions_are_valid` adds the cross-field rule the per-field enums cannot state:
  failure_class in the kind's own set, `none` exactly on success, registry only on skill rows.
  Pre-split rows (no failure_class/registry) still validate.
- A refused dependency consent returns its reason as `_ConsentRefusal`, a str carrying the
  closed class (`non_interactive`, `deps_declined`, `manifest_invalid`), so publication no
  longer classifies by matching the user-facing copy. The text the user reads is unchanged.
- `extension_registry` imports the hub's `_SOURCE_ID_ALIASES` instead of a hand copy.
- test file: schema read uses utf-8-sig (windows-footguns).
2026-10-06 11:10:43 -07:00
Teknium
ce14639879 fix(telemetry): a partial update with a failed build or restart keeps that failed_stage
update_cmd_fleet._verify_fleet_after_update writes `partial` for a failed build
(`not update_complete`) and for an incomplete restart as well as for a
verification gap. The relabel marked the synthetic verify stage failed for every
`partial`, so failed_stage moved from build/restart to verify and every such run
added a failed hermes.update.stage verify row. Verify is now failed by `partial`
only when no stage mark already failed (a stale/down fleet row still fails it).
The appendix row states the rule.
2026-10-06 11:10:43 -07:00
Teknium
717d8a3e5e fix(install-metrics): count the install note on a consented day; purge receipts on every opt-out
report_pending_installs recorded the fresh-install row at the first start,
usually the opt-in day itself (setup asks inside the install). The sender's
CONSENT_GATE_SQL needs period_start >= opened_at, so that day's package is
never sent: the metric mostly measured re-runs over already-consented homes.

- A receipt is recorded only when collection is on AND (sending is off, or an
  open send consent window opened at or before today 00:00Z); otherwise it is
  kept for the first start on a later day. Tested against the real store,
  real reconcile_send_consent and the real CONSENT_GATE_SQL: kept on the
  opt-in day, recorded the next day, and that package passes the gate.
- Every opt-out answer purges pending_installs AND pending_updates:
  save_consent(enabled=False) (setup, pre-chat offer, dashboard banner,
  `hermes setup telemetry`) and the tui_gateway shared_metrics.set RPC, via
  one shared_metrics_process.purge_pending_receipts used by begin_process too.
- Receipts are claimed by a plain rename, so an unparseable or non-dict
  receipt is deleted instead of sitting forever; a receipt no start counted
  within 7 days is deleted unreported (it would count under a later version).
- With Relay instrumentation off the store "settles" a row without recording
  it; the reporter now leaves the receipts for a start that records, instead
  of latching and deleting them uncounted.
- Docs: the hermes.install.run row states the consented-day rule, the 7-day
  cap, opt-out deletion and the interrupted rules.

Invariant tests red at 6246dd0f69d (5 in test_shared_metrics_install_run,
1 in test_shared_metrics_consent_rpc).
2026-10-06 11:10:43 -07:00
Teknium
74e0225377 feat(observability): hermes.install.run from a local installer receipt
install.sh / install.ps1 leave one closed-token receipt per full ladder run
(installer, outcome, failed_stage, failure_class, start/finish epoch) under
$HERMES_HOME/telemetry/shared_metrics/pending_installs. Every fail()/Fail call
site passes a closed failure class; the installer never sends anything.

The next Hermes start records each receipt once (latched by id, deleted after
the row is saved) only while collection is on; a start with collection off
purges the receipts unreported. Out-of-contract receipts are dropped.
2026-10-06 11:10:43 -07:00
Teknium
ab6f123eb4 docs+smoke(telemetry): failure_class/registry appendix rows; relay smoke asserts a real failed plugin install and a failed update run 2026-10-06 11:10:43 -07:00
Austin Pickett
ccff4df98c fix(skills): trust-scope inline-shell auto-exec to non-community hub skills
Inline `!`cmd`` expansion in SKILL.md bodies ran for every skill whenever the
global skills.inline_shell flag was on — a community hub skill and a builtin
were expanded identically. The hub's install gate (INSTALL_POLICY) blocks a
community install on the caution/dangerous verdicts the inline-shell DSL scans
as, but --force or a pre-scanner install puts that skill on disk anyway, and
the next view/slash/preload would auto-execute it on the host.

Gate expansion at the single preprocess_skill_content choke point (all three
surfaces route through it): community-trust hub lock entries never expand;
trusted/builtin hub entries and anything without a hub lock entry (bundled-
synced, user-created, project/external) keep the flag's contract. Lock read
failures skip the gate, like every other provenance consumer.

Fixes #63307
2026-10-06 11:20:43 -04:00
Teknium
38e83857e5 docs(update): name the git path's intended behaviour changes (kshitijk4poor 5411135640)
What: a "Behaviour changes on the git path" section in the source-update developer guide: ZIP
fallback only from an untouched checkout, arm failure stops before any move, startup syntax
judged at the target by an admitted interpreter in one cat-file spawn, moves by commit id with
HEAD verified, the upstream ff judged before it moves, and a gone-target marker kept over a
dirty tree.
Why: the reviewer asked which of these are intended; each is, and release notes need the list.
2026-10-06 06:22:27 -07:00
Teknium
0e430b2d82 fix(update): the launch repair runs a published closure only when it hashes to pre's blobs (M1, m3, m4)
A published recovery closure was trusted on presence alone: a power loss after the rename
could leave a torn _early_recovery.py that failed every launch forever, and foreign bytes
ran in place of pre's code.

- publish_recovery_closure writes a MANIFEST of blob ids (ls-tree at pre, each cat-file
  output re-hashed against its id), fsyncs files and directories before the rename,
  re-verifies an existing closure instead of trusting it, and refuses a pre that is not a
  full commit id (no HEAD, no path under the git dir).
- The minted launcher hashes every file against the manifest before importing; a missing,
  torn or foreign closure is discarded and rebuilt from git's objects (--no-replace-objects)
  and verified again.
- Its git is the recorded absolute one or an absolute PATH entry, never a bare 'git' that
  CreateProcess (or an empty POSIX PATH entry) resolves from the current directory (m4).
- A repair refused by a live writer exits 1 with the repair's own reason: no "repairing it"
  banner, no import traceback (m3).

(cherry picked from commit 94e436325b40b5b99962c3c646d3760ecad9dc4f)
2026-10-06 06:20:32 -07:00
Teknium
b4f65a5967 fix(update): with no published closure the launcher reads it from git's objects (D1)
A marker written by an updater that predates publication (or whose publication failed) left the
launch fallback with nothing to run. The launcher now materializes the same recovery/lock/custody
files from git's objects at the marker's pre (the marker's recorded git, else PATH's) into
<git dir>/hermes-update-recovery/<pre>/ by an atomic rename, then runs it exactly like a
published copy. A pre lacking any of those modules is never used. The launcher's fallback moves
to a readable module-level template (_CLOSURE_REPAIR).

(cherry picked from commit bcfa9973e4b59fdff5f2226dc7728c84738a7a11)
(cherry picked from commit 1cb50349c96cd82e650c4e9f81d47309efe0eb08)
2026-10-06 06:20:32 -07:00
Teknium
c18755d69e fix(update): a launch repairs a move that tore the repair's own code (D1)
The launch-time repair (hermes_bootstrap -> _early_recovery under the checkout lock) lives in
the tree a killed git rewrites. A merge SIGKILLed while writing hermes_bootstrap.py,
_early_recovery.py, a prefix of hermes_cli/__init__.py or a custody module left every launch
exiting 1 with the marker kept and the tree never restored.

arm_tree_move now publishes the recovery/lock/custody modules, as committed at the move's
pre commit (git cat-file, never the working tree), to <git dir>/hermes-update-recovery/<pre>/
by an atomic directory rename before it writes the marker. The minted launcher catches a
failing checkout import; with the marker present and its pre's closure published it purges
checkout modules, runs restore_interrupted_pull from that copy (same claim and checkout lock:
a live writer still refuses) and relaunches from the restored tree.

(cherry picked from commit 2bc12d9283d90a54f970a354a52f0a95d41bbfb0)
(cherry picked from commit 924b2a9a2d4dde6bc2a9074ff3ee0cd8f87c8684)
2026-10-06 06:20:32 -07:00
Teknium
d565161188 fix(update): refuse recovery when checkout custody code is torn
(cherry picked from commit 8b278db3e9e36b042d2f1f6ff95451449d28f37e)
(cherry picked from commit 7ab8e3763439753ae204868a3ebbd5fd0ce0dd28)
2026-10-06 06:20:32 -07:00
Teknium
b25d9db393 fix(update): docs name the parked-stash exit 1 and the owed completion
hermes_cli/AGENTS.md and cli-internals.md stated "after the commit point
nothing fails" without the one documented exception (an autostash whose
restore conflicted stays parked: `partial`, exit 1, #122557) and without
the lost-completion outcome P2 introduces (a completion that could not
start or died without a result is an owed `completion` follow-up, tail
re-armed, exit 0). update_receipt's module docstring still said receipts
live under <HERMES_HOME>; they live under the ROOT home
(get_default_hermes_root()), never a sticky profile's.

Review P6.
2026-10-06 03:44:49 -07:00
Teknium
741f29d517 docs(update): post-commit contract in the compacted update-pipeline docs
main moved the update-pipeline long form from hermes_cli/AGENTS.md into
developer-guide/cli-internals.md (f3af936694). The C3 contract (nothing fails
after the commit point; root-home receipts that are on disk from the start)
now lives there, and the AGENTS.md rule bullet states it in the 30k chain budget.
2026-10-06 03:44:49 -07:00
Teknium
4a5b2ea4c1 docs(update): profile sync is owed only when the step escapes; one post-commit Windows resume (m6, M4) 2026-10-06 03:44:49 -07:00
Teknium
3ba84d13e0 docs(update): receipt-store and gateway-marker rules after the commit point 2026-10-06 03:44:49 -07:00
Teknium
f71418d4f8 docs(update): dependency follow-up, Ctrl-C after commit, root-home readers, fleet exit 0 2026-10-06 03:44:49 -07:00
Teknium
045615ac5a fix(update): after the commit point nothing fails hermes update (C3) 2026-10-06 03:44:49 -07:00
Teknium
f9798bcf9b fix(tts): validate a plugin's stream rate once; keep nous off plugin routing
Independent-review findings on the plugin PCM streaming path:

- stream_sample_rate was checked, then re-read and int()-ed in the
  adapter: NaN/inf raised out of resolve_streaming_provider (the speaker
  then played nothing) and 0.5 became sample_rate=0. Validate once
  (finite, >= 1 Hz) and hand that int to _PluginPCMStreamer.
- `nous` is an alias of the openai path in _get_provider but not a
  reserved name, so a pinned `tts.streaming.provider: nous` could stream
  a plugin registered as `nous` while the sync path speaks OpenAI.
- Document that stream() must be thread-safe: the speaker prefetches up
  to 3 sentences, unlike the serialized synthesize() path.
2026-10-06 00:52:41 -07:00
Teknium
10f5530967 fix(tts): plugin PCM streamers keep the sync dispatcher's voice and routing
Follow-up to the salvaged plugin-streaming commit. The adapter read
voice/model from the plugin's own tts.<name> section, so a streamed reply
spoke the plugin default while synthesize() got tts.voice: the voice
changed with the surface. It also skipped the sync dispatcher's routing:
a same-named `type: command` provider (which wins for synthesize) lost
to the plugin on the streaming path, and an is_available() == False
plugin was streamed anyway.

- tools/tts_tool_plugins: one routing guard (_plugin_route_key) and one
  voice/model/speed builder (_plugin_voice_kwargs), shared by
  synthesize() dispatch and the new _plugin_pcm_streaming_provider
  (streams_pcm + positive stream_sample_rate + is_available(), read at
  resolve time)
- tools/tts_streaming: _PluginPCMStreamer stays a thin adapter;
  stream_channels/stream_sample_width dropped (the contract is int16
  mono, which every sink assumes)
- agent/tts_provider: streams_pcm / stream_sample_rate declared on the
  ABC with the contract
- tests: two invariant tests next to the plugin dispatch tests (the
  streaming test module skips without numpy); docs for plugin authors
2026-10-06 00:52:41 -07:00
Teknium
b5025017d5 feat(telemetry): ask once more where a "No thanks" may never have been seen
Before the type-ahead fix, an Enter pressed while `hermes chat` started
could save "No thanks" before the offer was on screen, and every surface
then treated the profile as answered. Every answer now also writes
`telemetry.shared_metrics.offer_version`. An "off" answer without it reads
as `reask`: undecided, so the CLI, Desktop strip and dashboard banner offer
it once more with a one-line reason. Any response settles it, including
Esc in the terminal and the banner close, which keep "No thanks", so
nobody is asked a third time. Opt-in answers were always deliberate picks
and are never re-asked.
2026-10-06 00:40:30 -07:00
Teknium
f12e6b66d9 feat(auth): brokered PKCE, expiry-driven rotation and aux on plugin OAuth rows
Generic plugin-OAuth support that the Solstice provider needs. None of it
names a provider:

- OAuthPKCEConfig.token_request: a confidential client whose secret lives
  behind a broker hands the grant to the plugin in place of a token_url
  POST. The old refresh_token is kept when the response omits one, which
  Google does on refresh.
- Plugin OAuth rows rotate 2 minutes ahead of their stored expires_at_ms
  (shared with the anthropic rule). Opaque bearers such as Google's ya29.*
  carry no JWT exp, so this stored expiry is the only clock they have.
- A row whose refresh grant just went DEAD stays DEAD on the inference 401
  that follows; before this it was re-benched EXHAUSTED and the dead token
  was replayed every cooldown.
- The auxiliary client serves an OAuth plugin that ships create_client from
  its pooled row on the main runtime's endpoint (compression, titles). It
  used to return "not directly supported".
- A 401 on a plugin OAuth row prints a re-sign-in hint, not the API-key
  advice.
- The Gemini native adapter exposes its method names, auth headers and
  error builder so a Bearer-auth subclass can reuse it without the API-key
  remedies.
2026-10-05 23:33:41 -07:00
Teknium
c0bd379b38 fix(telemetry): compression verdict reads the stored class; consent offer names the reason
Review follow-ups for the failure_class split: finish_compression_attempt now
decides skipped/failed from the normalized class it records (a padded or
mixed-case skip name no longer reads failed with a skip reason, and a
non-string class no longer raises); the pre-chat consent offer names the new
reason like the setup copy; the setup copy keeps 80 columns; the doc says a
refused batch's op rows carry the class of the op that stopped it.
2026-10-05 22:07:41 -07:00
Teknium
c99299d746 feat(telemetry): memory and compression rows say why they failed or were skipped
Fleet telemetry shows built-in memory writes failing 34-47% and automatic
compression failing ~15%, and several merged fixes each target one cause
(#133038 for memory, #133054/#133040/#133051/#133032 for compression). Both
metrics only carried outcome, so the fleet could not tell which cause a fix
moved.

- hermes.memory.op.count and hermes.compression.count gain one closed-enum
  dimension, failure_class; `none` on success.
- Memory: every refusal/failure return in tools/memory_tool.py and
  tools/memory_tool_store.py names its class (no_match, ambiguous,
  over_budget, would_empty, drift, read_failed, retry_cap, scan_blocked,
  missing_content, missing_old_text, invalid_args, stale_entry, disabled,
  staged, gate_refused) through a context variable; the result the model
  sees is unchanged. Plugin providers report provider_error / exception.
- Compression: the attempt log's own failure_class, collapsed onto the
  compressor's closed names (exception:<Type> / rollback:<Type> keep only the
  prefix, anything else reads other, a classless failure unknown). Skipped
  rows carry the skip reason, and the skip set is now defined once in the
  contract.
- Package schema v4 / event schema v4; v3 field sets of both metrics stay
  valid so pending rows drain. Doc table rows and the consent copy (CLI
  setup and every Desktop locale's collectedActivity) name the new reason.
2026-10-05 22:07:41 -07:00
Teknium
a928a959e3 feat(plugins): let plugins register Automation Blueprints via ctx
Plugins can now call ctx.register_automation_blueprint(key, *, title,
description, schedule_template, prompt_template, category, slots,
deliver_default, skills, tags). The fields mirror the built-in
AutomationBlueprint, and slots are BlueprintSlot field dicts, so the call
also crosses the plugin-host boundary.

- One lookup, cron.blueprint_catalog.list_blueprints() / get_blueprint(),
  merges the built-ins with the active profile's plugin blueprints at call
  time. /blueprint (CLI/TUI/messengers), /api/cron/blueprints and the
  instantiate route all use it. Each dashboard route resolves inside the
  ?profile= scope, off the event loop.
- Keys are namespaced <plugin>:<key>, so a plugin cannot shadow a built-in.
  A ':' in the key, a duplicate key, an unknown placeholder, a bad slot type
  or a schedule that does not parse is warned and ignored at registration.
- Catalog entries carry source/plugin. Desktop fetches the catalog for the
  profile it creates jobs in, keyed per profile, and shows the plugin name
  on the blueprint row and in the "Start from" select. The dashboard shows
  a plugin badge.
- A created job is an ordinary cron job with the prompt and schedule already
  filled in, so it keeps running if the plugin is later disabled.
- The ctx content registrars (register_skill, now joined by
  register_automation_blueprint) move to hermes_cli/plugins_content.py.
  The dashboard's blueprint types move to web/src/lib/automation-blueprints.ts.

Closes #132373. Requested by @enckequity.
2026-10-05 17:34:36 -07:00
Teknium
952322ca3c fix(plugins): inherited aux slots follow their base until the user pins one
Salvage follow-up for #129189 on top of #129209:

- An unknown or self-referential `inherit_from` now logs a warning and
  registers the task without inheritance instead of raising, so one bad
  base no longer fails the whole plugin load (the #129191 approach).
- `provider: auto` on an inheriting task means "no preference", like the
  empty strings: "Reset all to auto" (CLI + dashboard) writes exactly that,
  and before this it pinned the slot to the main provider while the model
  was still inherited from the base (mismatched route).
- Once the user pins a route on the task (non-auto provider, model or
  base_url), provider/model/base_url/api_key/api_mode/key_env/effort all
  come from the task's own block. Mixing per key let a pinned provider pick
  up the base's base_url and api_key.
- The CLI picker resolves the inherited view only for inheriting tasks.

Tests collapse to behavior contracts: base followed and changes
propagate, picker placeholders keep following, a pin wins whole, bad
bases warn and are ignored, re-registration cycles are cut.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-05 17:24:40 -07:00
thanhan-a17
56afae600e fix(dashboard,desktop): surface plugin-registered auxiliary tasks in Models settings
`PluginContext.register_auxiliary_task` already lists a plugin task in the `hermes model`
picker and layers its defaults into `auxiliary.<key>`, but every REST/UI surface behind the
Models page enumerated only the built-in `_AUX_TASK_SLOTS`:

- `GET /api/model/auxiliary` never listed the task
- `POST /api/model/set` (scope=auxiliary) rejected it: `400 unknown auxiliary task`
- `__reset__` and the empty-task broadcast skipped it; `_stale_aux_pins` ignored it
- Desktop Settings → Models and the dashboard ModelsPage hardcoded the built-in list

So a plugin's side model could only be pinned by hand-editing config.yaml per profile.

Fix, at the existing seams:
- `web_server_config._aux_task_slots()` = built-ins + plugin keys from
  `get_plugin_auxiliary_tasks()`, fail-soft; every consumer in that module uses it.
- `GET /api/model/auxiliary` reads plugin tasks INSIDE `_profile_scope`, so a request for
  profile B enumerates B's plugins (the plugin manager keys on the same context-local home) —
  the profile-scope gap flagged on #40922. Built-in rows are byte-identical; plugin rows are
  appended with `label`/`hint`/`plugin` from the registration.
- Desktop `model-settings.tsx` and dashboard `ModelsPage.tsx` render served plugin rows after
  the built-ins using the server label/hint (pure `auxTaskRows`; no new i18n strings, no-op on
  older backends).

Tests: real plugin loaded from a temp HERMES_HOME through PluginManager discovery, two
profiles in one process (listing scoped per profile; foreign task still 400), assignment /
broadcast / reset / stale-pin over a plugin slot, discovery failure leaves built-ins intact —
all red on main. Vitest covers the pure row merge and the rendered plugin row.

Fixes #40880. Salvages #40922.

Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com>
2026-10-05 17:24:40 -07:00
Apostol Apostolov
be3f0fcc1c feat(plugins): let auxiliary tasks inherit a base slot at read time
register_auxiliary_task's defaults start from a fixed shape and every key
is a full replace, so a plugin whose side model is a near-copy of a
built-in has to restate the whole block and silently drifts when that
built-in's shape changes.

inherit_from names a built-in or already-registered task whose effective
config becomes the base, resolved at read time so the plugin tracks the
base's current values instead of snapshotting them. Precedence is base,
then plugin defaults, then user config.

Self-referential inherit_from is rejected at registration rather than
left to the read-time guard: same-owner re-registration is allowed, so
the bad value would otherwise surface as unbounded recursion in a config
read. The guard stays for a hand-edited registry.

Closes #129189
2026-10-05 17:24:40 -07:00
Teknium
63f5e0ea45 feat(computer-use): pluggable single-select computer-use providers
Third-party drivers (e.g. a Codex-style computer-use reimplementation) could
only replace cua-driver by patching core. computer_use now follows the
single-select provider model Hermes uses for memory providers, context
engines and cron schedulers:

- plugins/computer_use/__init__.py discovers bundled
  plugins/computer_use/<name>/ then user $HERMES_HOME/plugins/<name>/
  (marker: register_computer_use_provider / ComputerUseProvider) and loads
  ONLY the provider computer_use.backend names (default "cua"), read per
  call so each profile gets its own. Unselected providers are never
  imported; listing reads plugin.yaml only. An unknown/unloadable name
  returns an error naming it and the installed providers; no fallback.
- The built-in cua-driver is an ordinary bundled provider at
  plugins/computer_use/cua/ (driver implementation stays in
  tools/computer_use). tools/computer_use/backend.py gains
  ComputerUseProvider (create_backend(permission_mode=...), is_available(),
  optional doctor()).
- The tool's check_fn and backend construction go through the selected
  provider; the model-facing schema is unchanged for every provider
  (prompt-cache invariant). The test-only HERMES_COMPUTER_USE_BACKEND env
  switch is gone (tests patch _new_backend).
- General plugin discovery treats computer-use providers as exclusive
  (not eagerly imported, not listed as toggleable) and skips the bundled
  computer_use/ category, same as memory/, context_engine/, cron_providers/.
- hermes tools and the Desktop toolset panel list installed providers next
  to cua; computer-use doctor/status/permissions and the Desktop status
  payload skip cua-driver/TCC probes when another provider is selected.
2026-10-05 17:20:30 -07:00
Teknium
6bf7de73e1 feat(browser): public captured CDP call seam for trusted plugins
Plugins that drive the agent's browser below the browser tools (Secure ENV,
catalog review on #132272) had no public way to send a raw CDP command over a
task's supervisor connection, so they bumped CDPSupervisor._next_call_id,
inserted futures into _pending_calls and sent on _ws. That breaks silently on
any internal change and collides between plugins.

SUPERVISOR_REGISTRY.capture(task_id) now returns a CapturedCDP pinned to the
supervisor's current socket and default page session:

  call(method, params=None, *, session_id=None, timeout=10.0) -> raw reply
  is_valid() / page_session_id / task_id / cdp_url

The validity check and the send run in one supervisor-loop step, so a call
never reaches a newer connection: after a reconnect, stop or registry
replacement every call raises CapturedCDPInvalid. Calls in flight when the
socket drops now fail at once (the supervisor fails pending replies on
teardown) instead of sitting out their timeout.

No model tool, config default or new capability: in-process Python could
already reach this. The handle enforces no origin/consent/ownership policy.

Reduced-scope salvage of #132752 (API shape and names from that PR); its
attach-reply handoff, old-wire cleanup and dispatch fences are not needed
because the supervisor closes the old socket on every reconnect, which drops
its sessions with it.

Co-authored-by: web3blind <264741654+web3blind@users.noreply.github.com>
2026-10-05 17:10:58 -07:00
Teknium
b813bb1b2d fix(telemetry): compression attempts that could not fail count as skipped, not failed
hermes.compression.count recorded every non-committed attempt as outcome=failed
except lock contention. Structural no-ops (insufficient_messages,
no_compressible_window, empty_post_handoff_window: the transcript is all
protected tail, no model call is made), a user stop (explicit_interrupt) and
superseded/stale attempts all landed in `failed`, so the fleet dashboard read a
session that simply had nothing summarizable as a broken compactor.

The outcome now comes from the resolved failure class (the previous commit keeps
the compressor's class instead of the caller's generic `no_progress`), and those
classes count `skipped`. Real failures (summary errors, would_grow, no_progress
after a real summary, stalls, commit errors) stay `failed`.
2026-10-05 16:18:47 -07:00
Teknium
19b57f11d6 fix(telemetry): latch a failed-connect episode only when it was recorded, per real adapter
Review fixes for the once-per-episode connect_failed dedupe:

- The episode was latched before the collection-consent gate, so a failure
  seen while shared metrics were off suppressed the first consented day's
  connect_failed. _record_connect now returns without latching when
  collection is off: only a recorded failure opens an episode.
- Local episode state was keyed by the privacy-projected platform name, so
  two custom adapters (both emitted as "plugin") shared one episode and one
  adapter's recovery closed the other's outage. The key is now the adapter's
  real platform value; projection happens only at emission.
- Docs: the hermes.platform.health row describes the episode semantics.
2026-10-05 16:01:43 -07:00
Teknium
ac9c8b4d02 fix(telemetry): a cancelled or lapsed provider sign-in is abandoned, not failed
hermes.provider_setup.count recorded every user cancel as event=failed
failure_class=cancelled, on every surface: Esc/Ctrl-C/Back in the CLI pickers,
Cancel on a Desktop/dashboard OAuth sign-in (DELETE /sessions), a cancel that
landed before the start response, and consent declined on the provider page.
A device code left to run out was also failed/other on the Nous, xAI and
Codex dashboard pollers (only the free-tier `timeout` reason and Codex
`expired` status were abandoned). Every provider's failure rate was inflated by
walk-aways (openai-codex desktop: 64 of 75 failed rows were `cancelled`).

- provider_setup_fields: failed + cancelled/expired is recorded abandoned, so
  every emitter (CLI, web routers, pollers) gets the same answer.
- setup_failure_class: a lapsed device code (DeviceCodeExpired on the Nous poll,
  AuthError code device_code_timeout/timeout, RFC 8628 expired_token) is
  `expired`; access_denied is `cancelled`; an error whose __cause__ is a network
  error is `network`.
- Dashboard/Desktop pollers keep the exception's closed class on the session
  (note_oauth_failure) so `status=error` no longer collapses to `other`;
  free-tier anon_unreachable is network, a start-route 504 is network.
- DELETE /sessions settles the metric immediately: a Nous/xAI poll blocks for
  the code's lifetime, so the cancel row used to land minutes later or never.

Live (real dashboard server, Desktop-owned, isolated HOME, real providers):
  base: nous/openai-codex cancel -> failed/cancelled (xai cancel never
        settled); nous code left to lapse -> failed/other
  fix:  nous/xai-oauth/openai-codex cancel -> abandoned/none, each at cancel
Live (real `hermes model` in tmux, Ctrl-C at the OpenRouter key prompt):
  base failed/cancelled, fix abandoned/none.
2026-10-05 15:36:14 -07:00
Teknium
d8b44813b1 fix(desktop): plugin settings pages keep their profile and identity
Review fixes for Settings ▸ Plugins:
- schema form is bound to one profile: keyed by (profile, plugin), renders
  only rows loaded for that profile ($agentPluginsProfile), and its save
  writes that profile or refuses; a scope switch discards the draft
- Capabilities gear carries its selected profile (&profile=), which
  Settings adopts into its scope before resolving the page
- overview / missing / empty states keep the Applies-to selector
- desktop pages and automatic agent pages use disjoint identities
  (kind-tagged uids; ?plugin= vs ?agent= routes)
- the folded schema form is addressed by agent key, so no plugin-chosen
  sub-page id (e.g. config) is reserved
2026-10-05 14:26:58 -07:00
Teknium
a731ffcc49 fix(desktop): plugin settings pages use native Settings rows
config_schema pages rendered raw keys as labels, ran the description into
the secret note without punctuation, floated Save mid-canvas and laid
fields on a two-column grid with an orphan cell.

- one ListRow per field (label + helper left, control right; JSON full width)
- label: manifest label/title, else the key in sentence case with acronyms
  kept (maps_api_key -> Maps API key)
- helper sentences joined with punctuation (joinSentences)
- Save on the page heading row (Passwords & Logins' pattern), disabled
  until dirty, spinner while saving
- only required fields are marked (settingsForm.optional -> .required)
- overview: Manage plugins as a heading-row button
- rail: a plugin row whose sub-page is open is named, not filled
- hello-runtime example: readable Chip style / gateway status copy
2026-10-05 14:26:58 -07:00
Teknium
6421b9f977 docs(desktop): document Settings ▸ Plugins pages; hello-runtime example
The hello-runtime runtime fixture registers a page with two sub-pages backed
by ctx.storage. The SDK guide gets a 'Plugin settings pages' section, plus
contract and export-table rows.
2026-10-05 14:26:58 -07:00
Teknium
c958e1a7b9 feat(plugins): on_human_input_request / on_human_input_resolved hooks
One observer pair fires wherever the agent blocks waiting on a person, so a
notifier plugin no longer needs a hook per prompt type or a `sudo` regex over
terminal commands (#132333, consumer: pushover-hermes-plugin).

Fire sites are the existing choke points every surface already routes through:
- sudo: tools/terminal_tool_sudo.py::_prompt_for_sudo_password (CLI callback,
  Ink TUI/Desktop sudo.request card, /dev/tty fallback)
- clarify: tools/clarify_tool.py::clarify_tool (CLI, TUI/Desktop, gateway)
- approval: tools/approval_prompt.py::prompt_dangerous_approval (CLI/TUI/ACP
  callback and input() paths), ::_present_with_selected_transport (plugin
  transports), tools/approval_gateway_wait.py::_await_gateway_decision
  (gateway platforms)

Payload: kind, request_id (shared by the pair), session_id, session_key,
platform, prompt; resolved adds outcome. prompt is always force-redacted and
the typed password / clarify answers are never passed. Smart (aux-LLM)
approvals and coalesced gateway followers do not fire it: no new human prompt.

Closes #132333
2026-10-05 01:15:16 -07:00
Teknium
61f8365da7 feat(plugins): ctx.current_cron_execution() tells a plugin which cron run it is in
Plugins could only see a cron run as the task_id string `cron:<job>:<execution>`,
with no way to tell a scheduled fire from a manual `hermes cron run`. A
security plugin that may only act inside a real scheduled occurrence had
nothing to check.

The scheduler now binds a frozen CronExecution (job id/name, execution id,
source, scheduled_instant, started_at, profile) in a ContextVar once the run
wins its claimed->running CAS (or a restart-safe worker adopted the row), and
resets it when the fire ends. ContextVar scoping keeps concurrent jobs and
profiles apart and follows the agent onto the threads the scheduler already
copies context into. Delegated children read None.

Also trims duplicated comments in _run_one_job_body to keep the facade within
its code-health line cap.

Closes #130722
2026-10-05 00:55:03 -07:00
Teknium
93c3abe35e feat(desktop-sdk): expose chatTextScale through host.settings
Core already owns a chat text size preference ($chatTextScale, Appearance ->
Chat Text Size, presets 90..175). Add it to the host.settings allowlist with
read, write and subscribe, bound to the same store atom and setter the native
control uses, so a theme/plugin like Hermes Quiet can follow or set reading
size without owning layout. Writes accept only the exact presets: the store
would otherwise snap a typo to the default and silently reset the user's size.

Docs: document the key, and list chat width / turn spacing / row geometry as
not-yet keys (no core preference exists; chat width tracks #55287).

Refs #131943 #131944 #131945
2026-10-05 00:24:49 -07:00
Teknium
f3af936694 docs: hermes_cli, gateway and desktop AGENTS.md keep rules, long form moves to docs
An agent working in an area gets root + that area's AGENTS.md as one project-context
block, so the area files must leave room beside the root. hermes_cli (24.2k -> 18.0k):
update-pipeline stage contract and free-tier long form -> cli-internals.md. gateway
(20.9k -> 17.2k): background notifications and /login long form -> gateway-internals.md.
apps/desktop (16.3k -> 9.4k): five judgment sections summarized with every invariant
kept, the reasoning -> new apps/desktop/ENGINEERING.md, the onboarding rehearsal ->
BUILDING.md.
2026-10-04 23:05:42 -07:00
Teknium
a4140eebb1 docs: root AGENTS.md becomes a hub; area rules move where they load
The root file is project context and was 38.7k chars, past the context-file budget
(20k floor, 30,720 on a 128k window), so every session lost its middle: routing table,
testing rules, code-shape rules. It now keeps the two invariants, a condensed rubric
(every rule kept), environment, structure, one-line rules that apply everywhere and the
routing table (11.8k).

Moved verbatim to where they load: Testing -> new tests/AGENTS.md; dependency pinning and
PM ownership -> new pm/AGENTS.md; machine facts -> new hermes_platform/AGENTS.md;
session-scoped tool availability -> tools/AGENTS.md; the full rubric with its real-close
examples -> developer-guide/contributing.md. Pointers in lint, checker messages, docs
and test comments name the new homes.
2026-10-04 23:05:42 -07:00
Teknium
3170e3cf9c docs(plugin-catalog): one listing per plugin lineage (rule 16) 2026-10-04 23:05:32 -07:00
teknium1
639eee3058 docs(plugin-catalog): README and docs pages describe the same delisting, removal and entry fields 2026-10-04 04:35:58 -07:00
teknium1
0f7a95f16d docs: Home Assistant moves to the plugin catalog 2026-10-04 00:08:20 -07:00
teknium1
7e19f4138b fix(telemetry): record auxiliary model calls with their real outcome
Every failed auxiliary model_route row read error_class=unknown because the
aux logical scope only carried outcome/model/provider and the projection
hard-coded unknown for any non-success. Every exception escaping call_llm,
including Hermes' own aborts (Ctrl+C / /stop mid-compression, interrupted
aux streams, task cancellation), was recorded as failed.

- classify the error that ended an aux call (and the last one a successful
  call recovered from) with agent.error_classifier; HTTP-200 bodies that
  carry a provider error object are classified from that object
- Hermes aborts (non-Exception BaseExceptions, InterruptedError) close the
  call as cancelled/none, matching the contract
- MoA aggregator streams report the same way
- aux calls that finish under the turn's own live scopes (title generation
  runs beside the turn) were never counted: the scope could not be popped
  and the turn-close drain closed it as an orphan without a route. The
  drain now closes it with the stored real result.
2026-10-03 13:38:29 -07:00
teknium1
4b2c2ae999 fix(telemetry): one-shot CLI runs report entrypoint one_shot, not interactive
Fleet telemetry labelled every `hermes -z` / `chat -q` / `-Q` run as
entrypoint=interactive because the cli surface mapped straight to
interactive and no caller declared anything else, so 95% of CLI sessions
read as one-turn humans and scripts could not be told apart.

- New closed entrypoint `one_shot` (contract + v3 schema enum). The task
  scope start declares it for cli-surface runs carrying the
  HERMES_SINGLE_QUERY_SESSION marker the one-shot paths already set (the
  signal session source and cache_ttl: auto read). Children Hermes
  dispatchers spawn (kanban worker source=kanban, A2A forward source=a2a)
  read the existing `background`. Delegated children, other surfaces and
  the REPL are unchanged.
- Engagement keeps treating one_shot like interactive (its prior label).
- `hermes -z` exits via os._exit, skipping the atexit hook that closes the
  metrics session: its pre-exit cleanup now runs the runtime shutdown, so
  task_run.finished and session.count are actually written (before, only
  task_run.started survived and the store kept a hot journal).
- Smoke expectations move to one_shot (it drives `chat --query --quiet`).
- relay-shared-metrics.md documents the entrypoint vocabulary.
2026-10-03 13:25:25 -07:00
alt-glitch
f1c79255fe feat(plugins): requires.gpu gates a plugin server on the host's GPU vendor
A portable plugin can now declare `requires: {gpu: nvidia}` for an MCP
server. On a host without an NVIDIA GPU, Hermes then refuses the
install, hides the server's tools, and the catalog card says "needs an
NVIDIA GPU". The check is the existing
`hermes_platform.host.facts.gpu_class()` read.

The requirement is independent of `app:`. The RTX Remix Toolkit is the
first consumer: it is a package the user unpacks anywhere, so there is
no install location to check, but it needs an NVIDIA GPU.

An unreadable GPU (`unknown`) passes, so a failed registry or sysfs
read does not lock out a machine that has the GPU. Only `nvidia` is
accepted: gpu_class() reports the highest-priority vendor, which
answers the question exactly for NVIDIA and not for AMD or Intel.

A registered server on such a host reports the new `unsupported_gpu`
state, carried through the gateway contract and the Plugins tab, with
a sentence that names the GPU instead of "Install <app>".
2026-10-03 10:26:12 -07:00
ethernet
5fe12f373e docs: drop source channels and install identity section; main is the only source channel
Remove the "Source channels and install identity" section from Updating &
Uninstalling, and stop documenting stable/canary as selectable for source
installs. `main` is the only valid source channel right now, so the
updating, CLI reference, and stable-releases pages say that instead.
2026-10-02 18:50:11 -04:00
ethernet
1a4508e2af Merge pull request #131691 from NousResearch/fix/release-abandon-cancels-runs
release.py abandon now cancels the abandoned rc's in-progress runs
2026-10-02 14:12:44 -04:00