Commit Graph

4460 Commits

Author SHA1 Message Date
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
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
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
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
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
mnajafian-nv
b4d7711480 docs(relay): document the summarizer fields as free-form identifiers
summarizer_provider and summarizer_model carry the resolved summary provider
and model ids as they are, but the module docstring and the docs said every
categorical field comes from a closed set, and the field table did not list
them. A consumer reading that contract would treat them as bounded labels.
List both in the field table as free-form and narrow the closed-set claim to
the other categorical fields.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit ce2020dbf354785d47c3002f431afe60c1046fa3)
2026-10-10 04:27:27 -07:00
mnajafian-nv
814cde892a fix(relay): mark a micro-compaction in a rotated child on the turn it ran in
The micro-compaction publisher resolved its Relay parent from the
compressor's current session only. After a rotating compaction earlier in
the turn that session is a child with no Relay scope, so the pass the
finalizer runs before the turn ends lost its mark, while the attempt, prune
and provider-native publishers already fall back to the session the turn
started in.

The finalizer now passes that session to the pass, and its mark takes the
same fallback.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 3519c942fad6ba2dce3b4475ca7ff56a02c7410f)
2026-10-10 04:27:27 -07:00
mnajafian-nv
f0afd34a9d fix(relay): keep the empty_transcript class on its attempt mark
The attempt record now logs empty_transcript when a context engine returns no
messages, but the hermes.compaction failure_class set did not list it, so the
compaction.attempt mark read failure_class: other and lost the reason. Add it
to the attempt-only classes and the documented set.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 0bdf840844f35e95361ae89edcb1264d2767663c)
2026-10-10 04:27:27 -07:00
mnajafian-nv
8810cf24b2 fix(relay): record why an overflow compaction ran in overflow_reason
overflow_reason is a v1 field, but it was always null: both overflow
callers passed only trigger="overflow", although each already holds the
error classifier's verdict for the provider rejection it recovers from.
A consumer could not tell a 413 from a context-length rejection or the
Anthropic long-context tier gate.

Both callers now pass the classified reason through _compress_context
into the attempt seed, so the attempt log line and the compaction mark
carry it. The mark keeps it in a closed set (context_overflow,
payload_too_large, long_context_tier, else other) and leaves it null for
every other trigger. The Relay docs list the set instead of calling the
field reserved.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit c3ad6f7fb8bfcc8f9c350f3a4272029d63333c7f)
2026-10-10 04:27:27 -07:00
mnajafian-nv
0edc904913 fix(relay): mark a second rotating compaction on the turn it ran in
With compression.in_place: false every commit moves the agent to a
child session, and that child has no Relay scope until a Relay-managed
LLM call or the next turn opens one. A multi-pass preflight rotates
twice before any LLM call, so the second attempt's record named a
session with neither a live turn nor a session scope, and its mark was
dropped at DEBUG.

The publisher now passes the session the agent's turn started in
(_inflight_turn_session_id, which the turn start already records because
compression can rotate mid-turn). _resolve_target keeps the same session's
live turn and then its session scope first; only when both are missing
does it fall back to that turn, and only while it is the live,
instrumented turn of that session. A child that already has a scope
keeps the mark there, because that scope owns the child's next LLM
starts and so must receive the freshness reset. The record's session_id
still names the child.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit af5c1c9bfde91105e88535a9235ceca6bd662887)
2026-10-10 04:27:27 -07:00
mnajafian-nv
eaaf948965 docs(relay): list the gateway reset after compression exhaustion as unmarked
The Compaction Marks section lists the history and request rewrites
that emit no hermes.compaction mark, but missed one: when a gateway turn
ends with compression_exhausted, the gateway replaces the session with a
new, empty one through reset_session (gateway/run_turn.py,
_hmwa_compression_exhaustion_reset) instead of compacting it. Nothing
on that path calls compress_context or the other mark emitters, so
readers should not expect a mark for the reset.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit ded1094b77585eb9b4af910881cc98226de7f809)
2026-10-10 04:27:27 -07:00
mnajafian-nv
2159dc3e78 fix(relay): align the hermes.compaction v1 contract with what Hermes emits
Review of the first cut of the marks found four places where the record
said something the attempt did not:

- A split that failed after it had stored the compacted history
  (failed_not_indexed without a rollback) returns the compacted
  transcript, so the model sees it next, but the record read
  aborted/failed and was named compaction.attempt. Relay then never
  reset freshness and the compacted history was never recorded. The
  commit path now logs history_rewritten (whether the returned
  transcript replaced the input), and such a record is a committed
  compaction with failure_class session_split_failed and split_status
  failed_not_indexed.
- in_place and session_rotated read false/false on a commit without a
  session database. They are now set only when split_status is
  in_place_committed or rotated_committed, and null otherwise.
- trigger_class read auto for a record with no trigger or an unlisted
  one. It now reads unknown there; an unlabelled automatic caller
  (trigger_source auto) keeps trigger unknown with trigger_class auto.
- The v1 sets listed values Hermes never emits (kind request_truncation
  and window_reduction, scope request, trigger recovery, outcome noop,
  token_count_method provider_usage, split_status pending and unknown).
  Consumers treated them as live. They are dropped from the v1 sets and
  documented as reserved for later versions.

session_ownership_unreadable joins the attempt-only failure classes.

The Compaction Marks docs now list the four sources exactly, what does
not emit a mark yet (OpenAI Responses native compaction and its
pre-checkpoint pruning, image shrinking after a provider rejection,
request-only truncation), the closed sets and null policy, why a Codex
app-server compaction keeps the compaction name (Relay records no LLM
start for those turns, so the freshness reset only affects a later
Hermes-side call) and is never marked twice, and two known gaps around
rotating compactions.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 6e6bc81fc0e074c88d1d632e8110d7490e203015)
2026-10-10 04:27:27 -07:00
mnajafian-nv
a64501e60c feat(relay): emit compaction marks from the compression attempt record
Hermes rewrites a session's history in several ways (batch summary,
deterministic fallback, micro-compaction, proactive tool-result prune,
Codex thread compaction) but never told Relay about any of them. A Relay
consumer could not see what was compacted, why, when, how long it took
or whether it went through compress_context, and Relay never reset
LLM-history freshness after a rewrite, so LLM starts after a compaction
stayed projected to the current user turn.

agent/compaction_events.py builds one flat, content-free
hermes.compaction v1 record per rewrite with closed enums (kind, scope,
official, method, trigger, trigger_class, outcome, failure_class) and an
other fallback, plus the effect counts and timings the attempt record
already carries. agent/relay_compaction.py emits it with the existing
scope.event API on the owning session's scope stack: under the live turn
of the same session, else under the session scope, else not at all.

A committed history or provider rewrite uses Relay's reserved
compaction mark name, which resets freshness for the session's agent
scope; every other outcome is a compaction.attempt mark. Records come
from the single attempt telemetry seam (so blocked, Codex-routed and
aborted attempts are covered), the micro-compaction telemetry record,
the proactive prune commit and Codex-native compactions Hermes observes.
No payload carries message, summary, focus-topic, error or path text,
and publishing never raises into compaction.

The marks are on whenever the Relay runtime is live; Relay exporters are
configured separately, so this adds no outbound path and no config key.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit dc5fd0875e7ec895b7826ee829bfc1b7f082de97)
2026-10-10 04:27:27 -07:00
mnajafian-nv
43fda4b388 fix(compression): report committed effects without leaking metric state
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)
2026-10-10 04:27:27 -07:00
mnajafian-nv
7ae7fac09c fix(compression): log one attempt record when an engine returns an empty transcript
The candidate guard refused an empty transcript from a context engine and
returned without an attempt record, so that compress_context call logged
nothing and the shared metric never counted it. Every other refusal in the
guard logs its own class.

Log it as an aborted attempt with failure_class empty_transcript; the shared
metric counts it once as failed under its closed-set "other" class.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 3f6a58fb3f2695a58d67fedb9f7410abff95ad58)
2026-10-10 04:27:27 -07:00
mnajafian-nv
c3af207f28 fix(compression): keep a late-unwinding attempt's own id, session and trigger
The attempt record read its identity from the agent's current-attempt fields,
which the next attempt overwrites when it begins. A stalled attempt that
unwound after its stall fallback had started therefore logged the fallback's
attempt_id, session_id and trigger_source, so two records claimed one attempt
and the stalled one disappeared.

Each attempt now carries its own seed on _Attempt and passes it to every
emit, so the record always describes the attempt that wrote it. Emits with no
attempt in hand (pool saturation) keep reading the agent's copy.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit a36db44d63888406702cab44d3ca29335727a6e2)
2026-10-10 04:27:27 -07:00
mnajafian-nv
490f4e4397 fix(compression): tell an unreadable session-ownership lookup from a lost one
_adopt_if_parent_rotated returns the same sit-out result for two
different exits: the lease found the parent already rotated by another
path (ownership lost), and the ownership lookup itself raised
("skipping compression this cycle", ownership unknown). Both were logged
as session_ownership_lost.

Each exit now writes its own record from inside the helper: a rotated
parent is skipped / session_ownership_lost, and a failed lookup is
aborted / session_ownership_unreadable, the same shape as the existing
cooldown_state_unreadable exit. Both records name the session the
attempt started in, not the child an adoption switched the agent to.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 83dfe1eaf6e630198734a65d8c13a26945ff43d9)
2026-10-10 04:27:27 -07:00
mnajafian-nv
4280f8b00e fix(compression): describe an aborted attempt from its own seed
An attempt record that is written before compress() seeds this
attempt's telemetry, or after a pre-commit restore, did not describe
that attempt:

- snapshot_stale and the early commit_fence_cancelled exit run after the
  attempt-start clear, so the record had no trigger_source;
- a commit fence refused after the summary restores the compressor's
  pre-attempt state, which puts the previous attempt's dict back, so the
  record carried the previous attempt_id, trigger, method and reclaimed
  tokens with this attempt's failure class;
- pool_saturated fires before any attempt begins, but still took the
  previous attempt_id from the agent.

_begin_compression_attempt now keeps the attempt seed (attempt_id, the
session the attempt started in, trigger) on the agent as well, since a
restore replaces the compressor's copy. When the compressor's telemetry
is not this attempt's own, the emitter starts the record from that seed
with method none and no token counts, and ignores the compressor's
fallback flags, which belong to the same earlier attempt. A refusal with
no attempt begun gets a fresh id and trigger unknown. Bypassed exits read
the same seed, so they also name the session the attempt started in
rather than a child it adopted.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit c29dba73a4c07c833951666f517a237e1e0fc65d)
2026-10-10 04:27:27 -07:00
mnajafian-nv
4c7ab8b2a0 fix(compression): name the automatic trigger and effect in attempt telemetry
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)
2026-10-10 04:27:27 -07:00
Teknium
5ba559c9e4 fix(relay): keep migrated Relay configs off 0.10's OTLP auto-export
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.
2026-10-09 19:58:29 -07:00
mnajafian-nv
9280fef70c chore(relay): tighten 0.10 upgrade validation
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-10-09 19:58:29 -07:00
mnajafian-nv
6caac042b1 chore(relay): upgrade to 0.10.0
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-10-09 19:58:29 -07:00
Teknium
07908be267 docs(update): CLI reference names stable as the default and --set-channel main as the way back
The reference still said hermes update pulls main by default and that main is
the only valid source channel, contradicting the updating guide.
2026-10-09 19:46:19 -07:00
Teknium
6a92a2aa49 feat(update): source installs default to the stable release channel
`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.
2026-10-09 19:46:19 -07:00
Teknium
097fcaa1de fix(skills): every turn announces skill changes once, on every surface
#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.
2026-10-09 19:31:10 -07:00
teknium1
ec78ae2211 feat(prompt): skills installed mid-session reach the model without a prompt rebuild (port cloudflare/cloudflare-os#267)
The persisted <available_skills> index is reused byte-for-byte across turns so
the provider prefix cache stays warm (#104414). A skill that lands after the
prompt was built (hub install, skill_manage from another session, org sync,
curator) therefore never reached the index the model actually routes on until
compaction rebuilt the prompt: skills_list is live, but the prompt tells the
model to scan the index, not to call it. cloudflare-os#267 found the same fault
in their chat-cached catalog ("a catalog states what a session can reach now,
which is not a fact a chat can hold").

Hermes' idiom for stored-prompt drift is a one-shot note behind the cached
prefix, not a rebuild, so agent/skills_index_delta.py stages the delta on the
same per-turn user-message channel the surface-switch note rides: the added
skills' own index lines (name + description, the routing signal), the removed
names, cumulative against the stored prompt and re-staged only when the delta
changes (read back from the api_content sidecar, so the gateway's fresh agent
per turn does not stack copies). An undone delta retires the stale note once.
The current index comes from the same cached builder the prompt did, so an
unchanged turn costs one LRU hit.
2026-10-09 19:31:10 -07:00
Teknium
87b5e05620 docs(skills): note inline-shell scoping for nested, external_dirs and unreadable-lock cases 2026-10-09 18:18:41 -07:00
Teknium
46d7718a52 docs(telemetry): v4 takes additive values in place after its stable release
Owner ruling for gateway_pause_failed: extend package schema v4 in place
(additive only; ingest validates the envelope and every shape already sent
stays valid) instead of bumping to v5. The two sentences that said v4 was
extended only while canary-only now say so.
2026-10-09 14:36:08 -07:00
Teknium
f549ce3fbd fix(update): drop config_unreadable: an unreadable config.yaml also hides consent, so no row is ever sent
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.
2026-10-09 14:36:08 -07:00
Teknium
2379b2e2eb fix(update): pre-apply exits that read exception/subprocess_failed name their reason
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.
2026-10-09 14:36:08 -07:00
Teknium
b1bba19558 fix(telemetry): the Windows hand-off's relaunched update child also reads its own claim as failed
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).
2026-10-09 14:22:08 -07:00
Teknium
975b62663a fix(telemetry): a Desktop hand-off's update refused by its own claim counts as failed
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`.
2026-10-09 14:22:08 -07:00
Teknium
18ee4251d8 fix(skills): hub installs download skill files from raw.githubusercontent.com, not one API call per file
`hermes-index rate_limited` (26 events / 9 installs in 14 days): GitHubSource.fetch
paid one Contents API call per file in the skill folder on top of repo + tree,
so an anonymous user (60/h) could never install a skill with ~58+ files and
four average skills drained the hour. Live on origin/main, anonymous:
skills-sh/remotion-dev/skills/remotion-best-practices cost 143 API calls and
failed rate_limited.

A file fetch pinned to the tree's commit sha now reads
raw.githubusercontent.com/<repo>/<sha>/<path> first: the same immutable bytes,
outside the REST limit. A raw miss (private repo, raw outage) falls back to the
Contents API as before. The same install now costs 2 API calls and succeeds
anonymously; anthropics/skills/pdf drops from 16 to 2.
2026-10-09 13:58:16 -07:00
Mariano Nicolini
879573d722 docs(tool-gateway): attribute the 429 retries to the gateway 2026-10-09 15:36:14 -04:00
Mariano Nicolini
4710e757ab fix(image_gen): fall back only when Krea provably created no job, and apply the Krea default on first-run setup too 2026-10-09 15:36:14 -04:00
Mariano Nicolini
a9ebde190a feat(image_gen): fall back to FAL when managed Krea refuses a request before creating a job 2026-10-09 15:36:14 -04:00
Mariano Nicolini
257849838f feat(image_gen): default new paid subscribers to Krea 2 Medium Turbo so Krea is the image lane without a config edit 2026-10-09 15:36:14 -04:00
Mariano Nicolini
bcf216d461 feat(image_gen): describe Krea image inputs as style references so the model stops rewriting the style into the prompt 2026-10-09 15:36:14 -04:00
ethernet
894a26a39e Merge pull request #135459 from NousResearch/fix/rc5-darwin-baseline
fix(release): stable releases pass signed-package acceptance when no upgrade baseline exists
2026-10-09 13:06:34 -04:00
Jeffrey Quesnelle
e0550c97bb Merge pull request #135438 from NousResearch/fix/local-recs-weak-hardware
fix(local-models): stop recommending the 27B on hardware that can't run it well
2026-10-09 11:19:21 -04:00
Ben Barclay
b8878816b0 docs(loops): soften backend-lifetime wording 2026-10-09 09:35:31 -04:00
Ben Barclay
4bb139921f fix(tui_gateway): keep a detached session alive while it owns an active /loop or /heartbeat
The per-session notification poller is the only driver of a Desktop/TUI
(route-less) /loop or /heartbeat. When the client disconnects, the WS-orphan
reaper collects the idle session ~20s later and the TTL/LRU reapers can
collect it after that; either stops the poller, so the schedule freezes
(status still 'active') until a client reattaches and the overdue tick fires.

Treat an active, non-gateway-routed loop or heartbeat like live delegated
work: the orphan reap re-arms instead of popping the session, and the TTL and
session-cap reapers skip it. Gateway-routed schedules are fired by the
gateway's own scanner and do not pin the session. The exemption is bounded by
the loop's tick budget and ends on stop/pause. Memory-trim quiescence is
unchanged: an idle looping session does not block a trim.
2026-10-09 09:35:31 -04:00
Teknium
73162b00ee fix(update): a Desktop-only Node dependency failure no longer blocks the TUI and web UI
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.
2026-10-09 04:16:17 -07:00
teknium1
8bff64d6ed fix(memory): keep a runaway-recall ceiling when prefetch spilling is off
Without the opt-in, external recall is kept whole up to
max(10 x hooks.output_spill.max_chars, 100k chars) and still spills above
that, so one runaway provider can't overflow the context window and wedge
the session. Normal provider-budgeted recall (~16 KB for Honcho) is
unaffected.
2026-10-09 02:59:16 -07:00
Erosika
2327290b0d fix(memory): make external prefetch spilling opt-in 2026-10-09 02:59:16 -07:00
Teknium
6a2909c6b1 fix(memory): a failed agent-start provider install is not retried by every process start
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.
2026-10-09 02:42:26 -07:00
Teknium
ecb66f60dd feat(desktop): plugin SDK pet bubble (ctx.pet.say)
Plugins can now put a short plain-text line in the core pet's speech bubble
instead of locating the pet canvas in the app DOM and floating their own
overlay over it (pet-wallet, #135178).

- ctx.pet.say(text, { id?, tone?, ttlMs? }) -> disposer; ctx.pet.clear(id?);
  ctx.pet.visible (ReadableAtom<boolean>)
- host-owned rendering in PetBubble, in-window (plugin lines only) and in the
  pop-out overlay (lines ride the existing pet-state IPC push)
- plain text, 120-char cap, control/bidi chars stripped, plugin-name label,
  TTL 6 s default (1-30 s), 3 live lines and 10 says/10 s per plugin
- core error / waiting states keep the bubble; nothing shows without a pet
- every line a plugin has up is cleared on unload/disable/reload
- docs: API section, migration-table row for pet-wallet, SDK exports
2026-10-09 02:04:56 -07:00
web3blind
aad11beae0 fix(browser): fence captured CDP dispatch and retain late replies
# Conflicts:
#	tools/browser_supervisor.py
#	tools/browser_supervisor_capture.py
2026-10-09 02:04:29 -07:00
ethernet
25a096f317 fix(release): treat any missing stable baseline as no upgrade arm
A release that skipped bundles never writes the stable baseline pointer,
so its absence is normal at any point, not only before the first bundle
release. Drop the final-tag scan. A 404 on the default pointer now always
plans no upgrade arm. A supplied baseline-manifest that does not exist
still blocks, because that is a bad input.
2026-10-09 01:41:35 -04:00
ethernet
4e23d79de4 fix(release): let the first bundle release pass without an upgrade baseline
No stable release has shipped signed packages yet. Every final tag so far
skipped bundles or predates the claim pipeline. So
releases/stable/release-candidates.json does not exist, and the three
"Pin actual OLD and NEW signed packages" jobs refused with "No published
stable package baseline".

transitions now treats a 404 as a first release only when no final stable
tag records a candidate manifest and no baseline-manifest input was given.
It still validates the receipt, emits empty matrices and baseline=none.
The packaged upgrade arms skip on that output. The gate accepts their skip
only when their own planner succeeded with baseline=none, so a missing
baseline after a real bundle release still blocks.
2026-10-09 01:37:59 -04:00