Commit Graph

51256 Commits

Author SHA1 Message Date
Teknium
74e59cd340 plugin-catalog: official plugins install from hermes-official-plugins
homeassistant, spotify and claude-subscription-directsdk now live as
subdirectories of NousResearch/hermes-official-plugins, the single repo for
Nous's official plugins (each old repo's history carried over). Plugin code
is unchanged, so the manifest surface is identical and the re-pin needs no
new consent.

Existing installs follow on their own: the left-core migration installs by
catalog name, and a catalog re-pin clones the entry's current repo/subdir and
rewrites the install record. Adds 1200x600 card images.
2026-10-10 16:25:22 -07:00
hermes-seaeye[bot]
b5722d6e49 fmt(js): npm run fix on merge (#136337)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-10 23:15:47 +00:00
Teknium
8f08b65538 fmt(desktop): prettier on the channel-display files after main's npm run fix pass 2026-10-10 16:05:48 -07:00
Teknium
cfcb5ea7bc test(desktop): the overlay channel fixture types its partial status 2026-10-10 16:05:48 -07:00
Teknium
26fa3aa079 fix(desktop): Change only where Settings can switch; name the release only on an explicit not-ahead
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.
2026-10-10 16:05:48 -07:00
Teknium
573f514a19 feat(desktop): version details name the update channel; the overlay links to change it
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.
2026-10-10 16:05:48 -07:00
ethernet
6a2662a4aa Merge pull request #135838 from NousResearch/ci-speed-pt2
More linting improvements
2026-10-10 18:09:58 -04:00
Maciej Lisiewski
334c7dee98 ci: don't enforce CC in pre commit 2026-10-10 23:58:48 +02:00
Maciej Lisiewski
0035c85145 fix: add tsx to linted extensions 2026-10-10 23:58:48 +02:00
Maciej Lisiewski
9b4551d824 chore: pin specific js dependency versions 2026-10-10 23:58:48 +02:00
Maciej Lisiewski
f8b76dd69b ci: add js linting to pre commit hook 2026-10-10 23:58:48 +02:00
Maciej Lisiewski
c098e0b80c ci: checkout once for docker and shell linting 2026-10-10 23:58:48 +02:00
ethernet
9456670d2f Merge pull request #135743 from NousResearch/harder-linter-fixes-part3
Harder linter fixes part 3
2026-10-10 17:55:39 -04:00
hermes-seaeye[bot]
6e5a56d947 fmt(js): npm run fix on merge (#136310)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-10 21:39:44 +00:00
Teknium
1f711b275f test(desktop): the source hand-off fixture installs its loopback transport once
baab4808d0 routed the stable lookup's GitHub reads through
source_check._request, so hermes_cli/source_check.py is now imported twice in
the probe (the CLI entry, then the module source_releases reads through). The
fixture's transport.py runs on every import of that file, so the second copy
wrapped the first and saw the already-rewritten 127.0.0.1 URL:
`assert parsed.hostname in ('hermes-assets.nousresearch.com', 'api.github.com')`
failed and the stable leg returned release-unavailable. main's JS & TS checks
have been red on this since 66605471e9.

transport.py now installs the redirect once (a marker on urllib.request). No
assertion changed. Locally: 1 failed on main, 1 passed with this.
2026-10-10 14:29:51 -07:00
Teknium
1c9135d5c2 test(e2e): the Desktop update harness keeps the seed's channel record
configureProvider() rewrote config.yaml from scratch for every spec, after
the seed had pinned `update.installs.<id>: main`, so the three Desktop update
specs still saw the stable default (release run 38078988253: the update check
answered {"channel":"stable","error":"release-unavailable"}). It now carries
the seed's top-level `update:` block into the provider config it writes.
2026-10-10 14:29:51 -07:00
Teknium
3a4b4b6f83 test(e2e): update sandboxes follow the main channel, not the stable default
#135847 made an official-origin source checkout default to the stable channel,
which resolves only from GitHub's latest published release. The upgrade-network,
Windows install+update and Desktop update harnesses keep the official URL as
origin, publish commits on main and never a release, so every update in them
stopped at the stable default:

- e2e-upgrade (network): "Could not resolve the stable source channel: No
  published stable release could be verified" (9 failed)
- Windows install + update E2E: "Release: stable v0.21.6; this checkout is
  newer and waits for the next release", checkout never moved (12 failed)
- Desktop update E2E (app-update, update-concurrency, build-fail): the update
  check answered {"channel":"stable","error":"release-unavailable"}

These suites test moving between commits, so each seeded install now records
`hermes update --set-channel main` right after its installer (new
_install_helpers.pin_main_channel; Windows Machine.install). Stable resolution
keeps its own coverage: test_release_channel_records sets stable explicitly on
the same seeded install, and tests/hermes_cli/test_source_release_channels.py.

Local: tests/e2e/core/upgrade/network on main plus this change, 21 passed
(4 failed in test_offline_and_flaky.py on main before it).
2026-10-10 14:29:51 -07:00
kshitijk4poor
b406d25f24 refactor(checkpoints): reuse _store_has_head in the footprint notice
The notice re-checked store/HEAD inline; tools/checkpoint_manager.py already
exposes _store_has_head() for exactly that. Also pin the notice test to
"across 1 project(s)" so a count of 11 cannot satisfy it.
2026-10-10 13:29:32 -07:00
Hygaard
e5d94d9537 fix(checkpoints): avoid history queries in the footprint notice 2026-10-10 13:29:32 -07:00
teknium1
84a244e6d6 docs(plugin-catalog): rule 11 — credentials come through Hermes, never from files
Plugins read declared secrets with agent.secret_scope.get_secret and use a
subscription sign-in only through credentialed_provider_request
(requires_auth), so Hermes attaches the token. They never read .env,
Hermes's auth.json, another tool's login or any credential store directly,
and never refresh, rotate or write OAuth tokens. Missing route = ask Hermes
for one. The plugin developer guide's own examples switch from os.environ to
get_secret to match.
2026-10-10 13:26:51 -07:00
Teknium
66605471e9 test: a relaunched hand-off update adopting its own claim runs on the macOS lane
The hand-off pid test only checked which pid the first update child was
told, and it was gated to Linux, so the macOS runner (where the refusal
was reported) never ran it.

It now drives the real posix.sh hand-off, has the update start a second
generation detached from it (the relaunch / takeover shape), and runs the
real UpdateLock there: on main that generation refuses with the custodian
as holder; with the fix it adopts. Gated "posix" so the macOS lane runs it
against marker.sh's whole-second creation times.
2026-10-10 10:44:34 -07:00
Teknium
7ef7df7773 test: read the hand-off view BOM-tolerantly 2026-10-10 10:44:34 -07:00
Teknium
4a4ccaee4d chore: map elvin-du's commit email for attribution 2026-10-10 10:44:34 -07:00
Teknium
cb070f86e1 test: the hand-off pid an update child is told is checked by running the hand-off
The salvaged test grepped posix.sh for the export line (tests must not read source). This
runs the real posix.sh hand-off against a launcher-backed stub install and records, inside the
update child, the HERMES_UPDATE_HANDOFF_PID it was given and marker line 1 at that moment:
they must be the same live custodian. Red on main: the child is told the script pid while
line 1 names the custodian.
2026-10-10 10:44:34 -07:00
elvindu
013e94b38a fix(install): name the marker owner as the Desktop hand-off pid
The posix hand-off hands marker line 1 to a custodian subshell
(marker_refresher_start) and then exported HERMES_UPDATE_HANDOFF_PID="$$" —
the hand-off script's own pid.

`hermes update` adopts a live claim only when the owner is its own pid, the
HERMES_UPDATE_HANDOFF_PID value, or an ancestor
(update_lock.UpdateLock._is_partner). The custodian is a *sibling* of the
hand-off script, so it is none of the three and the ancestry walk can never
reach it: every desktop-initiated update refused the claim its own script had
just made and exited 2 ("Another Hermes update is already running ...
process <custodian>"), leaving the Desktop to relaunch and retry forever.

Export $MY_PID instead: the custodian once the handover succeeded, and the
script's own pid when it did not.

Fixes #134268
2026-10-10 10:44:34 -07:00
Teknium
cb80675ab3 fix(update): stable lookup failures travel as exceptions and name the quota the failed request spent
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.
2026-10-10 10:44:23 -07:00
Teknium
baab4808d0 fix(update): the stable lookup sends the user's GitHub token and names a rate limit as one
`_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".
2026-10-10 10:44:23 -07:00
Teknium
4070f011a2 feat(plugins): core-made Codex requests so plugins never hold the token
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.
2026-10-10 10:43:32 -07:00
rob-maron
f86276d2eb nous portal reasoning content echo 2026-10-10 21:45:19 +05:30
kshitijk4poor
a62979dc36 fix(compression): a refused candidate's attempt record claims no effect
Review thread on #136050 (agent/context_compressor_telemetry.py, "Refusals
report unsaved effects"). The compressor records a candidate's effect
fields before the commit checks run. When the growth guard refused the
candidate, or a database failure rolled the transcript back, the aborted
record still reported its `messages_after`, `tokens_after`,
`tokens_reclaimed` and `items_dropped`. That claimed reclaimed tokens for a
conversation that stayed unchanged.

Drop those fields from any record that neither committed nor rewrote the
history. The input-side counts stay.
2026-10-10 20:18:10 +05:30
kshitijk4poor
b862bfaaf0 fix(compression): keep a running attempt's record when an overlapping call stops at a gate
Review threads on #136050 and #136051 (agent/conversation_compression.py,
"Overlapping calls erase attempt details" / "Skipped calls erase active
records"). `_begin_compression_attempt` cleared the compressor's three
attempt-telemetry fields and rewrote its seed at entry, before the
automatic gate and the session lease. A second automatic call that stopped
at the gate (e.g. a cooldown while overflow recovery ran with
`bypass_cooldown=True`) therefore erased the running attempt's record. That
attempt could still commit, but logged `method: none` with no counts and no
fallback class, so a fallback commit read as a clean shared-metric success.

The previous-attempt clear (#118580) now runs in `_run_summary_phase`, under
the lease, just before the summary dispatch. The same statement writes this
attempt's own seed, so the entry-time seed write goes. Calls that stop at a
gate, the lease, or a stale snapshot never touch the shared fields.

A rewrite that survives a later bookkeeping failure (`failed_not_indexed`
after the atomic archive) also gets its counts finalized from the saved
transcript, using the same `rewritten` result that names the Relay mark
(thread "Saved tails disappear from counts" on #136051).
2026-10-10 20:18:10 +05:30
kshitijk4poor
31d1bd3491 fix(scripts): count only committed attempts as batch compactions in the micro-compaction report
Review thread on #136050 (agent/conversation_compression_telemetry.py,
"Refusals inflate batch counts"). Since #135807 every exit of
`compress_context` (blocked by a guard, skipped, aborted, refused) logs one
`context compression attempt telemetry:` line, and
`scripts/micro_compaction_report.py` counted every such line as a batch
compaction. Keep only records whose `commit_status` is `committed`.
2026-10-10 20:18:10 +05:30
kshitijk4poor
4eaa67e901 refactor(compression): import _safe_int from its new home instead of re-exporting it
Review thread on #136083 (agent/context_compressor.py:36). #135807 moved
`_safe_int` into `agent/context_compressor_telemetry.py` but imported it
back into `context_compressor.py` only so `micro_compaction` could keep
reading `_cc()._safe_int`. AGENTS.md forbids re-export shims for internal
moves: `micro_compaction` now imports it directly.
2026-10-10 20:18:10 +05:30
kshitijk4poor
5265f9e070 fix(relay): mark an in-place engine's archived compaction as a compaction
Review thread on #136083 (agent/conversation_compression.py, "Committed
rewrites look failed"). The commit-path attempt record decided
`history_rewritten` with `compressed is not messages`. A context engine may
compact its input list in place, so the returned list IS the input even
after `archive_and_compact` stored it. If a later bookkeeping write
(`update_system_prompt`) then fails, the record read
`history_rewritten: false`, the Relay mark was `compaction.attempt`, and
Relay skipped the history-freshness reset although the model now sees the
compacted transcript.

Use `commit.compacted_in_place`, which `_commit_compaction` sets on the line
after the atomic archive returns, alongside the identity check.
2026-10-10 20:18:10 +05:30
kshitijk4poor
62fb70d564 fix(compression): name the guard that blocked an overflow attempt
Overflow recovery calls the automatic gate with `bypass_cooldown=True`, so
the summary-failure cooldown cannot be what blocked it. But
`_compression_block_reason()` always reported the cooldown first, so with a
cooldown also active:

- the attempt record said `blocked:cooldown` while the structural backoff
  or the ineffective breaker was the real blocker, and
- `_mark_compression_blocked_transient` marked an `ineffective` block as
  transient, so overflow recovery deferred instead of counting toward
  exhaustion, which #97488 says must never happen.

`_compression_block_reason` takes the gate's `ignore_cooldown` flag.
`_automatic_compression_gate_blocks` reads the reason once, with the same
flag the gate got (older engines keep the no-argument calls), and returns it;
the transient signal and the blocked attempt record both use that one
reason, so the telemetry module no longer re-derives the guard order.
2026-10-10 20:18:10 +05:30
kshitijk4poor
760c93fef7 fix(shared-metrics): count an empty-transcript refusal under its own class
#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.
2026-10-10 20:18:10 +05:30
kshitijk4poor
54471e04a4 refactor(compression): drop the unread attempt-seed copy and log attempt records from one helper
Follow-up to #136083 (salvage of #135807):

- `agent._compression_attempt_seed` had one reader, the `_attempt_seed`
  fallback, which every production emit bypasses by passing `attempt.seed`;
  the only seedless emit (pool saturation) passes `attempt_began=False` and
  never matched it. Removed.
- Both attempt emitters log through `_log_attempt_record`, so the line
  `scripts/micro_compaction_report.py` parses has one source.
- The in-place coverage call reuses `original_messages`, and the
  `_commit_compaction` docstring lists the anti-growth guard before memory
  extraction, matching the order #135866 introduced.
2026-10-10 20:18:10 +05:30
mnajafian-nv
a7445c5081 fix(shared-metrics): limit bench alias to committed rows
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>
2026-10-10 20:18:10 +05:30
mnajafian-nv
4810c637d4 test(shared-metrics): cover a committed summary_overload_degraded attempt
The parametrized test covered four of the five fallback classes a
committed attempt keeps on hermes.compression.count, and missed
summary_overload_degraded: the class the compressor sets when sustained
summary-model overload ends in a deterministic summary. It takes the
same path as the other four, so this adds the fifth case to the same
test rather than a new one.

With the classifier and finish_compression_attempt restored to the
base, the case reads success/none and fails with the other committed
fallback cases.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-10-10 20:18:10 +05:30
mnajafian-nv
0a81c37ed0 fix(shared-metrics): keep a committed fallback's class on hermes.compression
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>
2026-10-10 20:18:10 +05:30
Maciej Lisiewski
038dd659b7 ci: limit python cyclomatic complexity to pre-commit hook 2026-10-10 16:37:15 +02:00
Maciej Lisiewski
dca4811c5f ci: duplicate required subset of pyproject rules in ruff conf 2026-10-10 16:15:28 +02:00
Maciej Lisiewski
a0b9927de9 ci: re-enable TID251 linter check everywhere 2026-10-10 16:07:48 +02:00
Maciej Lisiewski
443ea25b57 ci: fix linter errors after rebase 2026-10-10 16:01:32 +02:00
Maciej Lisiewski
d95e35fe0f fix: side effect of linter fixes 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
dd7cb3ab1b ci: address review comments 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
d68bb0c9ae ci: make ruff strict the default 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
ae0994cd15 ci: re-fix some linter errors after a rebase 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
bfd01ac80d ci: use new, stricter ruff config 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
c57fbf4584 ci: fix remaining ASYNCXXX linter errors 2026-10-10 15:20:14 +02:00