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.
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)
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)
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)
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.
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.
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.
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
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.
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.
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.
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.
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.
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>
`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>
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
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.
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>
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`.
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.
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.
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
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
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.
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
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
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
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.
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.
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.
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.
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>".
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.
Keep newly observed sleep credit until the following poll because the supplied idle duration can predate that sleep. Extend both contributor regression tests across the sampling boundary and qualify the platform guarantee.
Add AwakeIdleMeter, which reads the wall clock and time.monotonic() on
each watchdog check. monotonic pauses during sleep on macOS
(mach_absolute_time) and Linux (CLOCK_MONOTONIC), so the gap between the
two clocks is time spent asleep, and that sleep is not charged as idle
time until the agent records new activity. The cron inactivity watchdog
and the turn liveness watchdog both use it, because on main both guard a
cron run with a 600 s wall-clock bound. A job that is really idle for
600 s awake is still killed.
Fixes#131523
(cherry picked from commit 07d9ddc24736e243d72327ff90a24f95e86119d8)
`release.py abandon` deleted the draft and wrote the marker but left the
attempt's Stable Release run going. A live run keeps building, and with
autopublish on it can publish the draft that abandon just cleared.
abandon now lists the claim ref's runs of stable-release.yml and runs
`gh run cancel <id> --force` on every one that has not completed. This
happens before the draft is deleted and before the marker is written: the
marker removes the attempt from the outstanding set, so a cancel failure
after it could never be retried. A published release is still refused
before anything is cancelled. The `delete` callable is renamed `execute`
since it now runs cancels too.
memory-providers.md gets a "Plugin catalog" note on the Supermemory section,
`hermes plugins install supermemory` in its setup, and a "Migrating from
bundled Supermemory" section that mirrors the Hindsight one. memory.md,
overview.md, integrations/index.md, cli-commands.md and the developer guide
no longer list Supermemory as bundled.
Mirrors the English pages: catalog note, install row and upgrade section on
memory-providers, catalog wording in tools/tools-reference, and the plugin's
cli.py link (blob URL, also fixed in the English developer guide).
Desktop, the gateway and a scripted `hermes update` have no TTY, so the
dependency consent gate refused every catalog provider that declares Python
deps: both migration hooks failed with 'Reinstall declined: dependency install
skipped (non-interactive)' and the provider stayed missing. The agent-start
hook exists precisely for those surfaces.
The configured memory.provider plus security.allow_lazy_installs (read for the
home being migrated) is the consent that let the bundled provider install its
deps on demand; carry it through dashboard_install_plugin ->
install_catalog_entry(assume_deps_consent=...). A terminal still prompts, and
allow_lazy_installs: false still refuses with the install one-liner.
Both seams are public contracts for plugin/theme authors (first consumer:
t3-code-theme, #129600); the page documents the arbitration, failure
isolation and lifetime guarantees so a theme never needs DOM reads.