Holographic has a standalone third-party maintainer (itpartypattaya) and a
reviewed catalog entry, so it leaves core now rather than on Oct 15. Restores
9e827685f2 exactly: reverts the revert (435b7bd3a7) and its health waivers
(b9ecc727e9). byterover and retaindb have no maintainer and stay bundled until
the announced date.
This reverts commit 9e827685f2 (#136424). It landed before the announced
October 15, 2026 date for holographic leaving core; it re-lands on or after
that date. Restores plugins/memory/holographic and its tests/docs.
Every `hermes update` on the main channel first fetched
hermes-assets.nousresearch.com/releases/channels/main.json. That record has
never been published; a 404 fell through to the git branch. Some regional
WAFs answer that same request 403 instead, which raised and stopped the
update outright (#128295; patched around in #128323 and #128305).
main IS the git branch, and the only thing its record could ever carry is a
retirement notice nobody has published. resolve_source_target now returns
the main branch target directly, the way stable reads GitHub releases
directly. Preview channels (e.g. pm-preview) keep their R2 records.
Holographic now ships from itpartypattaya/hermes-plugin-holographic through the
plugin catalog (plugin-catalog/holographic.yaml, v0.6.1 @ 46b6f908, a fork of
Nous's archived handoff copy). It keeps the provider name, memory_store.db and
plugins.hermes-memory-store, and memory_provider_migration already lists
holographic in _LEFT_CORE. Deleting the in-tree copy and its tests lets
`hermes update` and agent start install the catalog plugin for every home that
has memory.provider: holographic.
Host code that still releases memory-store handles on profile unserve/delete
resolves the store through import_provider_module, the same way profiles.py
already does, and no longer imports plugins.memory.holographic. Tests that used
holographic as "a bundled provider" now use byterover. The journal-mode
contract drops the holographic row because the plugin owns that store now.
People who installed the archived Nous repo by URL have no catalog record, so
`hermes plugins update` kept pulling the archive. read_catalog_sidecar now
adopts an install from that repo as the holographic catalog entry, and update
re-pins it to the maintained repo at the reviewed sha. That catalog re-pin
also needed the reviewed-pin scan trust that catalog installs already get. Without
it, update_plugin scanned the reviewed tree as an unreviewed community source
and blocked on caution findings, and that blocked every catalog re-pin of
holographic (the reviewed 46b6f908 tree has 14 caution findings).
- A steer merged into a tool_result turn no longer ends the in-flight tool loop,
so last-turn-only Claude models keep the loop's thinking.
- CLI /branch copies reasoning_content like the gateway and TUI branches do.
- The preflight estimate prices Gemini/Copilot signatures and the Converse
sidecar at 0 like the compaction tail walk; a mismatch loops compaction.
Every model reasons and intends that reasoning to be replayed, but the
chat-completions wire echoed reasoning_content only to DeepSeek/Kimi/MiMo/
Ollama (plus a hardcoded Portal stealth model) and reasoning_details only to
OpenRouter; turn_context popped `reasoning` everywhere, so Ollama's /v1
(reads only `reasoning`) never saw any.
- message_sanitization owns one route->carrier table
(_REASONING_CARRIER_ROUTES): strict schemas (Mistral, Groq, Cerebras,
SambaNova) get nothing, Fireworks reasoning_content only, OpenRouter-format
gateways (OpenRouter, Kilo, Vercel AI Gateway, Nous Portal) all three,
Novita/MiniMax /v1/TokenHub + reasoning_details, local/Ollama/Upstage +
reasoning, everyone else reasoning_content.
- The " " pad stays limited to the must-echo tier (DeepSeek, Kimi, MiMo);
Ollama leaves it (no echo requirement) and the stealth hardcode is gone.
- Reactive recovery: a 4xx unknown-field rejection that NAMES a reasoning
key this request carried is recorded per (provider, host, model) and the
retry re-shapes api_messages without it. Generic upstream errors never
count; a recorded key falls through, so no loop. History is untouched.
- Nous Portal reads reasoning_details again: the #118182 "replay budget"
does not exist in the Portal and does not reproduce live.
- Gemini thought_signature survives on Gemini hosts/providers even when the
model id is an alias.
- Vendor keep-reasoning flags (docs-only): GLM thinking.clear_thinking=false,
DashScope preserve_thinking / clear_thinking, Kimi K2.x thinking.keep=all.
- stale_thinking_reaches_wire follows the same table, so compaction trigger
and tail walks agree with what is sent.
Review on #136313 (Greptile):
- The overlay offered "Change" to a custom-branch checkout and to runtimes
older than --set-channel, but Settings shows no selector for either, so
the link landed nowhere. The label stays; the action is passed only when
sourceUpdateChannel() allows a choice.
- An older checker omits aheadOfRelease yet may also pin an ahead checkout
to its own HEAD, so a missing field read as "on the release". source_check
now always sends the boolean, and the status line names the release only
on an explicit false.
The updates overlay (status-bar version click) showed no channel, so a
source install could not tell it was pinned to stable releases.
- VersionDetails (shared by About and the overlay) gains a Channel row for
source checkouts: "Stable releases", "Every commit (main)", or
"Branch: <name>". Packaged installs render no row; their channel is the
package's.
- The overlay passes a "Change" link that closes the dialog and deep-links
to Settings > About > Updates, where the selector and its confirm step
live. The channel is not switchable from the overlay itself.
- Current on stable, the status line names the release ("You're on the
latest stable release (v0.21.6)."). A forward-only checkout ahead of the
release keeps the generic line: source_check now reports
`aheadOfRelease`, since its pinned target is HEAD and would otherwise
look identical to being on the release.
Review on #136086 (Greptile):
- The failure no longer travels through a module dict. `_resolve_release`
raises; `_resolve_stable` describes the exception; `resolve_source_release`
stays the `(None, None)` wrapper for its other callers.
- A token GitHub rejects (401) retries anonymously; a 403 on that retry was
described as the user's GITHUB_TOKEN quota. `_request` now marks the
raised error with whether that request carried a token.
- The test server closes its listening socket and joins its thread.
`_resolve_stable` read releases/latest + commits/<tag> through a bare
urllib call with no credential, so it spent the anonymous 60/hour per-IP
budget even with GH_TOKEN set or gh logged in (the rest of the update check
already uses github_api's token ladder). Behind one office, CI or container
exit that budget runs out, and the failure was then reported as
"No published stable release could be verified".
GitHub API reads now go through source_check._request (token, gh CLI,
anonymous; a rejected token retries anonymously). The R2 reads keep their
own path, so the token never leaves api.github.com. A lookup that fails on
transport now raises describe_github_failure's line (rate limit + reset
time + the GITHUB_TOKEN hint when anonymous) instead of "no release".
Plugins that need the user's ChatGPT/Codex subscription (hermes-live-voice mints
an OpenAI Realtime client secret) vendored their own reader + refresher for
~/.codex/auth.json. Codex refresh tokens are single-use, so that second refresher
races Hermes's locked refresh and can revoke the user's whole token family.
hermes_cli.plugin_provider_requests.credentialed_provider_request() lets core make
the request instead: it resolves the credential through
resolve_codex_runtime_credentials (Hermes's locked refresh, profile-scoped),
attaches Authorization itself, sends only to chatgpt.com / api.openai.com, never
follows redirects, and returns status/headers/body. Callers are attributed by the
plugin directory on the call stack and must declare `requires_auth: [openai-codex]`
in plugin.yaml; the declaration shows at install, in `hermes plugins show`, and a
catalog re-pin that adds it asks for consent. A plain importable function so
dashboard plugin_api.py modules can use it.
#135807 makes the empty-transcript refusal emit its attempt record through
the shared-metric lifecycle (on main that exit recorded nothing), but
`empty_transcript` was not in the closed `COMPRESSION_FAILURE_CLASSES` set,
so the `hermes.compression.count` row read `failed / other`. Add the class
to the contract, the v4 package schema enum and the metric docs so the row
says what happened.
Keep aborted attempts' existing classification when the summary model was
benched. The new regression case proves the failed row remains other while
a committed fallback still maps to feasibility_skip.
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
finish_compression_attempt reported every committed attempt as
outcome=success with failure_class=none, so the metric could not tell a
clean summary from a commit that went through a fallback: a
deterministic summary (summary model failed, pre-LLM feasibility skip,
repeated stall, sustained overload) or the main model writing the
summary after a separate summary model failed.
A committed attempt still reads success (it did commit and break the
prompt cache), but now keeps the attempt log's own class when it has
one: summary_generation_failed, feasibility_skip,
stall_deterministic_fallback, summary_overload_degraded or
aux_model_fallback. summary_model_benched, the deterministic commit after
the summary model is benched for repeated fallbacks, maps to
feasibility_skip, which is the pre-LLM skip it is. Any other class reads
other.
No new enum values, so the v4 package schema is unchanged and a fallback
row still validates. Consumers that read failure_class != none as a
failure must now also check outcome: on a success row it means
"committed with a fallback".
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
Finalize effect counts from the transcript that crosses the commit boundary, including retained partial-compression tails and boundary anchors. Explicitly discard bypassed shared-metric state so a later refusal cannot consume stale attempt metadata.
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit ddf3b229a9e2fdee651f36afbec974717d334f76)
The content-free attempt record is the one place that says why a
compaction ran and what it did, but it could not answer either question:
- Every automatic caller passed no trigger, so idle, turn-start
threshold, engine preflight, pre-API, post-tool and gateway hygiene
attempts all logged trigger_source=auto. Each call site now passes its
own label; the shared metric still buckets them as auto.
- tokens after, message counts and how the summary was made were
computed and logged as prose but never reached the record. A finished
rewrite now records messages_before/after, tokens_before/after,
tokens_reclaimed (signed; a summary can grow a short transcript),
token_count_method=estimate_rough, method (llm_summary,
aux_fallback_main, deterministic_fallback), items_dropped, and the
tool_results_pruned / reasoning_items_pruned pass counts.
- Attempts that never reached the local compressor returned silently:
the automatic breaker gate (before and under the lease), every Codex
app-server exit, a lease that found the session already rotated, and
an unreadable durable cooldown. Each now logs one record with a
commit_status (blocked, skipped, failed, aborted, committed), a route,
and a class such as blocked:cooldown. These stay out of shared metrics,
which never counted them.
Test doubles for _compress_context gain the trigger keyword the call
sites now pass.
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 1795993d1a01d91dd76e6ec824cb606f8a03b897)
Review findings on #135949 (Greptile, @unsupportedpastels):
- An older runtime (v0.21.6) answers checks but rejects --set-channel, so the
selector failed on every click. source_check now marks its reply
`channelSelectable`; Desktop shows the row only when the install says so.
- The confirmed choice painted only after the forced check returned. The row
now shows it at once, and a failure re-reads the install instead of guessing.
- A checkout following a custom branch rendered as "Every commit" (main). The
row now appears only for stable, or for main itself.
The unchosen stable default is forward-only, but _head_containing treated
`merge-base --is-ancestor` exit 1 as proof HEAD lacks the release. In a
shallow clone that exit is a guess at the history boundary, so a checkout
already past v0.21.6 was "updated" back to it; an unreachable GitHub
compare did the same. Only proof (exit 1 on full history, or GitHub
saying behind/diverged) moves the checkout now; unknown keeps HEAD.
Relay 0.10 starts gen_ai trace/log/metric export on its own whenever an
active configuration meets an OTEL_EXPORTER_OTLP_* endpoint in the process
environment, and those spans carry prompt and response messages. The
legacy HERMES_NEMO_RELAY_ATIF/ATOF variables never exported OTLP, so the
generated relay-plugins.toml now pins opentelemetry.enabled = false.
Also restore the discovery-only test's DISABLED-state assertions: they
pass on 0.10 and only trip on a host with a machine plugins.toml.
Publishing a vX.Y.Z GitHub release is the whole promotion: the source
updater's stable channel now reads only GitHub's latest published release
(non-draft, non-prerelease, strict vX.Y.Z) and verifies the tag on origin.
It no longer asks R2 for releases/channels/stable.json first, and it
ignores the R2 release-candidates pointer, which the stable pipeline writes
before the GitHub draft is published.
`hermes update` and the Desktop update button on an official source
checkout now move between published vX.Y.Z releases instead of tracking
main commit by commit, and `hermes update --set-channel stable` works.
- stable resolves through R2 when a record exists; until one is published
it falls back to the latest published GitHub release (non-draft,
non-prerelease, strict vX.Y.Z, origin tag verified). It used to fail
with "Channel object not found: releases/channels/stable.json".
- default_channel(): an official-origin source checkout defaults to
stable; forks, mirrors and checkout-less trees keep main. Package
installs (MSIX, Store, bundled, electron-updater) are untouched.
- An install that never chose a channel only moves forward: a checkout
already newer than the latest release (every install that tracked main)
stays put until a newer release ships. --set-channel stable is the
explicit consent to land exactly on the release commit.
- Same rule in --check, the apply path and the passive source_check the
Desktop and banner use.
#118661's note only fired on the stored-prompt restore path, which a long-lived agent (classic
CLI, TUI, Desktop) runs once per conversation: those surfaces never saw a mid-conversation skill
change. The check now runs from build_turn_context every turn against the prompt the turn
actually sends.
- What was told is kept on the agent for its prompt; a fresh agent (gateway, -q --resume) reads
the newest note back from the WHOLE transcript instead of the last 200 rows, so a long session
no longer re-announces an unchanged delta (review by @kvnloo).
- A compaction rebuild that now lists the announced skills stays quiet.
- Index rows split on ": ", so qualified plugin skills (plugin:skill) parse.
- The plugin go-live note drops its own skills list: the per-turn note covers plugin skills like
any other, so a plugin install no longer announces its skills twice.
The timed-out cat-file read is caught in target_syntax_error (the preflight) instead of
read_target_files, so the preflight prints one line saying the post-update check still runs;
publish_recovery_closure keeps suppressing the timeout as before.
The exit is real (RuntimeError from require_readable_config_before_write), but the consent gate
reads the same config.yaml and reads it as collection off: a live run on a scratch install with
an unparseable config.yaml wrote no metrics store at all. A closed value nothing can emit only
widens the contract.
hermes.update.run rows with apply_mode=unknown still had three exits with no stop_class on main:
- The startup-syntax preflight (update_cmd_commit.read_target_files) runs `git cat-file --batch`
under a 120 s limit. On a blobless install (install.sh clones --filter=blob:none) every changed
blob lazily fetches in its own round trip, so a slow link times out and the uncaught
TimeoutExpired ended the whole update as `subprocess_failed` after 2+ minutes (31 CLI + 23
Desktop install-days). An unread batch now counts as unread files, which is the preflight's
documented contract; the post-move syntax check stays the backstop.
- An unreadable/unparseable config.yaml (require_readable_config_before_write RuntimeError) or an
invalid configured channel name (ChannelError) while picking the channel read `exception`. They
now record `config_unreadable` (new) / `channel_unresolved`.
- A failed Windows gateway pause (_abort_on_error / ServicePauseFailed RuntimeError) read
`exception`; it now records `gateway_pause_failed` (new).
Two closed UPDATE_STOP_CLASSES values, the v4 schema enum and the relay-shared-metrics doc row
change together; every legacy value stays valid.
#135348 ran ruff's UP037 autofix with ruff.strict.toml's target-version = "py314". On 3.14
annotations are lazy (PEP 649), so stripping quotes is safe there; on 3.11-3.13 a function or
variable annotation is evaluated when the def or statement runs. Every unquoted annotation that
names the enclosing class (`def clone_for_agent(self) -> ContextEngine`), a class bound later in
the module, or a TYPE_CHECKING-only import now raises NameError on import: 300 modules on main
(gateway.config, run_agent, hermes_state, cron.jobs, the Telegram adapter, ...) fail to import
under 3.11, the update path crashes mid-`hermes update` on any install whose interpreter is
older than 3.14, and the e2e-upgrade suite's retry leg fails with
`NameError: name 'ContextEngine' is not defined`.
- Re-quote exactly the 79 annotations (36 files) that name something unbound when they are
evaluated (including tui_gateway/server.py's `_TurnScopes`, which only reaches that module's
globals through register() after import); no other text changes.
- agent/turn_scripted_prelude.py: `Generator[...]` gets its third argument back. d76cce4f11
applied UP043 (default type args, valid only from 3.13) to a module-level alias, so
`Generator[Y, S]` raises TypeError on 3.11/3.12 and takes gateway.run, agent.conversation_loop
and tui_gateway.{server,ws,entry} down with it.
- ruff.strict.toml target-version = "py311", the requires-python floor. With it UP037 leaves
these quotes alone and only strips the two that are genuinely redundant, which this commit
strips; any future version-gated fix stays valid on every supported Python.
- tests/ci/test_strict_ruff_target.py pins the target to the requires-python floor (red on main).
On Windows the hand-off names the process it created as the marker's delegate, and
hermes_bootstrap's relaunch (subprocess.call) or the legacy .cmd's cmd.exe makes the updater
its child, so the delegate check now also accepts our parent. The docs sentence moves to the
hermes.update.run outcome row and names the checkout-lock case (another mutator holding it).
The hand-off quits the Desktop and launches `hermes update` as the marker's
delegate. When the update lock then refuses that claim (the pre-a28a5d03a9f
whole-second creation-time bug on macOS, still present on installs that cannot
pull the fix through the button), no other updater exists: the Desktop shows
"Hermes couldn't finish updating" and the user retries. The metrics row read
outcome `refused`, which the rollup treats as neither attempt nor failure, so
~320 Desktop update failures a fortnight vanished from the failure rate.
Record that one case (marker names this process as its delegate) as `failed`;
a refusal under another update's claim stays `refused`.
Post-fix telemetry still showed `hermes skills install llm-wiki` failing as
failure_class=filesystem_error, source=hub. Two restore paths failed for good:
- A skill dir left behind without its SKILL.md (a manual delete that kept user
files, or a copy that died partway) made copytree raise EEXIST on every retry.
The restore now copies around the leftovers (dirs_exist_ok); a dir with no
SKILL.md is no skill, and user files in it are kept.
- A skill present behind a symlinked category (which the loader follows, so it
works) was missed by the rglob scan, so the restore ran into the containment
guard and failed. The canonical SKILL.md now counts as present.
A restore that still fails (read-only dir, a symlinked category with the skill
missing) was recorded as source=hub, because the failed exit returned no bundle
and _record_skill_install fell back to "hub". The bundled branch now marks the
attempt origin=bundled, so every bundled-path outcome, raised or returned,
records source=bundled.
Every source update installs Node dependencies as one npm ci over ui-tui,
web and, when a built desktop app is present, apps/desktop. Only
apps/desktop brings native addons (node-pty, electron), so a host whose
compiler cannot build them (Ubuntu 20.04's g++ 9 rejects -std=gnu++20)
failed the whole install and lost the TUI and web UI builds as well, with
"a source update could not be finished automatically" on every launch.
When the union fails and the desktop app is part of it, retry once without
apps/desktop: the TUI and web UI build, the desktop build is reported owed
("desktop dependencies failed", the line the hand-off scripts match), and
the update names `hermes uninstall --gui` for hosts that never run the
app. The same hint prints whenever a headless Linux update builds Desktop,
since such a host keeps rebuilding an app nobody launches.
hermes gateway restart --all (and status, cron's gateway check, the update
fleet sweep) matched gateway argv host-wide with no owner check, so a gateway
run by a different UID became a SIGTERM target ('Permission denied to kill
PID') and was reported as an unfolded per-profile gateway with
'hermes gateway migrate --multiplex' advice. Non-root callers now skip
processes another uid owns; root keeps the host-wide scan (sudo over a
User= system unit), and an unknown owner keeps the argv verdict.
A memory provider's own installer (mnemosyne-hermes install, its README's
manual `ln -s`) leaves ~/.hermes/plugins/<name> as a symlink into a venv. The
catalog install of the same name then failed in _sanitize_plugin_name with
"Invalid plugin name 'mnemosyne': resolves outside the plugins directory" and
was recorded as failure_class=manifest_invalid, though the catalog manifest is
valid (hermes plugins validate passes it). Name the link and its target, say how
to free the slot, and record already_installed.
recover_at_startup tried the configured provider's catalog install once per
PROCESS, so a home whose install fails (PM resolve/clone error, offline) retried
and failed again on every `hermes chat -q`, cron run, gateway agent and Desktop
restart. Each attempt is a recorded hermes.extension.install.count failure:
hindsight logged 1048 failures over 204 failing install-days (~5 per day per
install, the retry-loop shape) after it left core.
The left-core migration already had the fix: a failure stamp under
<home>/cache gates startup attempts for an hour while `hermes update` always
retries. Move that stamp into memory_provider_migration and use it in
recover_at_startup too; the backed-off start still tells the user memory is
off and names the install command.