Commit Graph

4682 Commits

Author SHA1 Message Date
emozilla 6d66598112 fix(local-runtime): CI failures — Linux asset names, GPU-less runners, subprocess encoding
Three portability bugs the Windows development machine hid:

- The version check's subprocess.run used text=True without an explicit
  encoding, which decodes with the locale codepage and crashes on
  non-UTF-8 bytes (the repo's Windows-footgun lint). Pass
  encoding='utf-8', errors='replace'.
- test_sha256_mismatch_rejects hardcoded the Windows asset name
  (bin-win-*.zip); Linux runners resolve bin-ubuntu-*.tar.gz, so the
  poisoned download was never the file under verification and the
  expected rejection never fired. Resolve the asset name the way the
  installer does.
- test_download_job_lifecycle_with_sha_failure let variant selection
  price against the host machine; a GPU-less CI runner honestly refuses
  every build (409) before the download path under test is reached. Pin
  a generous budget — the test is about hash failure, not selection.
2026-08-14 01:47:54 -04:00
emozilla aa4bf1ec2b feat(local-runtime): model catalog and dashboard routes
A curated model catalog priced for the machine it's viewed on, and the
/api/local-models/* routes the desktop consumes.

- catalog: each model ships a quant ladder (Q8 down to Q4, best first)
  with exact sizes and sha256s pinned from Hugging Face LFS metadata.
  Selection picks the highest-quality build whose weights + 64K-floor KV
  fit GPU memory entirely; machines that can't get Q4 spilled to system
  RAM; refusal only when even Q4 exceeds GPU + RAM. Nothing below Q4
  ships — the quality loss is too severe for a first local-AI
  experience. Split-GGUF variants, vision projectors, and spec-decode
  draft models download as a unit, each file verified.
- routes: status (cheap, poll-safe), hardware, catalog (each row carries
  plain-language fit facts the UI shows verbatim), download jobs with
  aggregate byte progress that survive the pane unmounting, activate
  (start server, set as main model — the click is the opt-in), eject,
  delete (removes every staged file), and server on/off. Downloads run
  8 parallel ranged connections and verify sha256 before use; a running
  router only scans models at spawn, so staging changes bounce it.
- inventory: staged local models appear as a provider row in the model
  picker payload every surface consumes; no credential — a local server
  is authenticated by reachability.

Catalog reachability (repos, filenames, live-sha drift) is covered by an
opt-in network test gated on HERMES_TEST_NETWORK=1.
2026-08-14 01:24:36 -04:00
emozilla b95e38757a feat(local-runtime): context policy — fit, grow, spill deliberately
Local models get one context contract: any model runs at any window up to
its native max; hardware and session depth only change speed. No knobs.

- gguf + estimator: a stdlib GGUF reader feeds a per-layer context-memory
  estimator that prices dense, sliding-window, and recurrent/hybrid
  layers separately. Per-architecture cost spreads ~40x (dense 144 KiB
  per token vs hybrid ~3 KiB), so per-layer pricing is what makes
  1M-token windows a launch decision instead of a guess. Estimator
  accuracy vs real models: worst case ~8%.
- context_policy: models launch at the largest window that fits GPU
  memory entirely, floored at 64K. On Windows/WDDM, over-allocating VRAM
  silently slows decode ~9x, so every window grant re-fits against live
  memory. When weights exceed VRAM, spill placement pins expert/FFN
  weights to host RAM so attention and KV stay resident (~1.75x over
  naive spill); speculative decoding turns on only for spilled configs.
- growth: when a session reaches its window's edge, Hermes grows the
  window toward native max instead of compressing — both compression
  gates try growth first, and compression becomes the move of last
  resort at native max, at the ~6 tok/s speed floor, or when physics
  says stop. Growth re-prefills server-side; the conversation history
  never mutates, so prompt caching is unaffected. Grown windows persist
  per model and re-fit honestly on every boot.
- presets: launch decisions travel to the router as a generated
  --models-preset INI; catalog sampling defaults merge under policy keys
  (policy wins), vision projectors and spec-decode drafts attach when
  present.
- model_metadata: the context meter reads the granted window from the
  running server, per router child, so the UI shows the window the model
  actually has.
2026-08-14 01:24:20 -04:00
emozilla adb2fdbf5c feat(local-runtime): managed llama.cpp server — install, supervise, resolve
Hermes can now bring its own inference engine. New hermes_cli/local_runtime
package:

- binaries: resolve and download official llama.cpp release builds for the
  host platform (CUDA/Metal/Vulkan/HIP/CPU), sha256-verified, with N-1 tag
  retention for rollback and honest errors for platform gaps.
- supervisor: spawn one llama-server in router mode with a generated API
  key; crash-restart with backoff (router only — child failures surface,
  never auto-retry); readiness proven by a real generation rather than a
  health probe; idle models unload after 15 minutes and reload on demand.
- detect: fingerprint an already-running llama-server via /props so an
  external server is used instead of starting a second one. Servers that
  merely speak /v1 (Ollama, LM Studio) don't false-positive.
- endpoint resolution: a llamacpp-flavored provider with no explicit
  base_url resolves managed-first, detected-external second; an explicit
  base_url always wins. No new provider surface — the existing custom
  provider aliases carry it.
- lifecycle: the backend boots the server when the user has opted in and
  shuts it down with the app so no orphan pins GPU memory. Config lives
  in the local_runtime section; deliberately no context or VRAM knobs.

The server binds 127.0.0.1 (never localhost — the name resolution adds
~2s per request on Windows) and models load on first inference rather
than at boot.
2026-08-14 01:24:00 -04:00
Shannon Sands 6977d21fa7 feat(gateway): disk-usage telemetry + dashboard disk-pressure banner (NS-656)
Extends the NS-656 memory-pressure surface to cover disk exhaustion
(OOF-2 / OOF-107 lineage: agents fill their data volume — SQLite writes
fail, sessions stop persisting — while every dashboard looks healthy).

- gateway/disk_status.py (new): collect_disk_status() samples
  shutil.disk_usage(HERMES_HOME) and classifies pressure
  (critical: <256 MB free or >=95% used; elevated: <512 MB free).
  Never raises — degrades to pressure="unknown" with null telemetry,
  same contract as collect_memory_status().
- /api/status: sibling `disk` block next to `memory`, advisory only —
  not folded into component/overall health.
- web: DiskPressureStatus type; MemoryPressureBanner generalized to a
  resource banner with worst-first triggers (disk critical > memory
  critical > OOM restart > disk elevated > memory elevated) and
  cascading dismissals — hiding the top trigger surfaces the next one
  instead of silencing everything. All dismissals stay boot_id-scoped.
- i18n: diskCriticalBanner / diskElevatedBanner (en, optional fields
  with English fallback per existing pattern).

Tests: gateway/test_disk_status.py (14), web_server disk-block
presence/degradation, banner disk trigger/priority/dismissal-cascade
suite (21 total).
2026-08-13 20:30:12 -07:00
Shannon Sands e11d1ddc7f feat(status): surface memory pressure and suspected-OOM restarts to users (NS-656)
Hosted agents can be OOM-killed hourly while the dashboard and the NAS
agent card both look perfectly healthy — every memory signal the gateway
already produces (heartbeat mem samples, lifecycle-ledger unclean-exit
verdicts, cache-pressure evictions) dies in server-side log files. The
BlueAtlas incident (NS-608) ran for three days like this.

This is the read-side fix:

* New gateway/memory_status.py distills the existing 30s loop heartbeat
  (gateway RSS + system MemAvailable/MemTotal + swap) and the lifecycle
  sentinel into a compact `memory` block: pressure ok/elevated/critical/
  unknown, coarse MB numbers, and last-boot unclean/suspected-OOM flags.
  Pure file reads, no new sampling, no gateway IPC. Stale (>150s) or
  future-dated heartbeats degrade pressure to "unknown" so a dead
  gateway's final gasp can't render a live "critical" banner forever.
  Critical thresholds mirror the ledger's OOM-suspicion heuristics: if a
  level would make a later unclean death "suspected OOM", warn at that
  level while the process is still alive.

* lifecycle_ledger.record_startup now carries prior_unclean_exit /
  prior_suspected_oom onto the reclaimed sentinel — previously the
  verdict survived only in append-only diag prose. Flags age out on the
  next sentinel rewrite (scoped to the life after the crash).

* /api/status serves the block (profile-aware, executor-offloaded,
  fail-safe to pressure=unknown). Deliberately NOT folded into
  components/overall: memory pressure is advisory, and flipping overall
  to "degraded" on it would page NAS's availability sweep for a
  condition the eviction valve is already handling. Public-safety:
  coarse numbers/enums/booleans only — same disclosure class as the
  existing nous_session_valid field, added for the same NAS-sweep
  audience.

* Dashboard: new MemoryPressureBanner (app-shell, next to
  ProfileScopeBanner) with worst-first trigger precedence
  (critical > suspected-OOM restart > elevated), per-trigger
  session-scoped dismissal, and escalation re-opening past a dismissal.
  i18n keys optional with English fallbacks, matching the
  managingProfileBanner convention.

Tests: gateway/test_memory_status.py (classification bands, staleness,
clock skew, corrupt files, bool-is-not-int), lifecycle sentinel
carry-forward, /api/status contract (block always present, collector
crash degrades instead of 500), and 7 banner component tests.

NAS-side ingestion (agent-card notice + memory-tier upsell) ships
separately.

Refs NS-656; context: NS-608, NS-657, OOF-77.
2026-08-13 20:30:12 -07:00
kshitij a3cda34137 fix(models): repair the two CI slices the default-flip broke
- web_server CONFIG_SCHEMA: fold the one-field models_dev category
  (models_dev.url) into the agent tab via _CATEGORY_MERGE, matching the
  established pattern for single-field categories (slice 7,
  test_no_single_field_categories).
- image_routing._lookup_supports_vision: pass allow_network=True to
  get_model_capabilities. The vision-capability lookup runs when an
  image actually needs routing (not per conversation turn), and the
  #31179 text-only-main guard depends on catalog data — with the new
  allow_network=False default a cold cache returned 'unknown', which
  falls back to attempting the call and reintroduced the #31179
  failure shape (slice 8, test_text_only_main_skipped_when_no_
  aggregator). This preserves that path's historical
  network-on-cold-cache behavior; the fetch stays 4h-TTL cached and
  backoff-limited.
2026-08-14 03:31:22 +05:30
kshitij acd8737c10 fix(models): ETag conditional GET, no-network hot-path invariant, mirror URL override for models.dev catalog
Harden the models.dev catalog refresh path (#35838) with three missing
pieces:

1. ETag conditional GET — every network request sends If-None-Match
   with the last-known ETag (persisted alongside the cache file). A 304
   Not Modified re-confirms the existing cache without re-downloading
   the full ~2 MB registry. This makes the 4-hour TTL effectively free
   to maintain.

2. No-network-on-hot-paths invariant — allow_network=False is now the
   default for every query function called on the conversation hot path:
   get_model_capabilities, get_model_info, lookup_models_dev_context,
   _get_provider_models. These are called during vision routing, image
   routing, cost-guard checks, and context-length resolution on every
   turn — they must never block on the network. Interactive flows
   (model picker, model switch) explicitly pass allow_network=True.

3. Mirror URL override — models_dev.url in config.yaml lets deployments
   point at a self-hosted mirror without code changes. Follows the same
   pattern as model_catalog.url.

Additional hardening:
- Cache TTL bumped from 1h to 4h (ETag makes refresh cheap)
- Corrupt/empty disk cache is rejected with a warning instead of being
  served as {} and silently breaking provider/model resolution
- _validate_registry() guards against non-dict and empty-dict payloads

Fixes #35838
2026-08-14 03:31:22 +05:30
kshitij cdd0a26031 fix: restore session model on resume instead of falling back to config default
Two bugs caused resumed sessions to use the config default model instead
of the model the session was actually using:

1. CLI /model switch didn't persist the new model to the session DB row.
   The gateway calls update_session_model() after a /model switch, but
   the CLI path only updated in-memory state and the agent's runtime —
   it never wrote the new model to the sessions.model column. So the DB
   row always kept the original model from session creation.

2. Resume didn't restore model/provider from the session DB row.
   _preload_resumed_session and _init_agent restored CWD and YOLO from
   session_meta, but never read session_meta['model'] back into
   self.model/self.provider. So even if the DB had the right model,
   resume would use whatever was in config.yaml.

Fix:
- _handle_model_switch / _apply_model_switch_result: call
  update_session_model() after a session-scoped /model switch (skipped
  for --once and --global), mirroring the gateway's behavior.
- New _restore_session_model() method: restores model/provider from
  session_meta on resume, with provider/base_url/api_mode from
  model_config.gateway_runtime. Also swaps the running agent in-place
  for mid-chat /resume.
- Call _restore_session_model() from all three resume paths:
  _preload_resumed_session, _init_agent, and _handle_resume_command.
- Track _explicit_model_override flag so -m/--model on the CLI overrides
  resume (user intent wins). Cleared on /new.
2026-08-14 02:15:22 +05:30
Jeeves Assistant 90dacec87e fix(plugins): report entrypoint capabilities in CLI 2026-08-13 13:38:58 -07:00
Jeeves Assistant 17dc773156 fix(plugins): discover entrypoint capabilities 2026-08-13 13:38:58 -07:00
kshitij de47d19f1f fix(models): one canonical override schema, fill-gap _default semantics
Review follow-ups on the model_overrides feature:

- ONE canonical override schema everywhere. get_model_info previously
  merged the override dict raw into the models.dev catalog shape
  ({**raw, **override}), so the documented context_window/supports_*
  keys silently did nothing on that path (cost guard, inventory) while
  working in capabilities/context paths — same config key, two
  incompatible schemas. Overrides are now translated into the catalog
  shape at the get_model_info boundary (_override_to_catalog_shape),
  and sub-dicts (limit, modalities) are MERGED, not clobbered — an
  override setting only context_window no longer wipes the catalog's
  limit.output.
- _default is now a FILL-GAP default, not an override: it applies only
  to models the catalog does not know (the #8731/#84482 self-unblock
  path) and never displaces catalog data. A
  _default: {context_window: 128000} can no longer clamp every model
  of a provider. Explicit per-provider+model entries keep their
  win-over-catalog semantics.
- Early-chain _override_context_window (model_metadata step 0b) is
  explicit-only, so a _default can never preempt custom_providers
  per-model settings or live probes; fill-gap defaults apply at the
  lookup_models_dev_context catalog-miss boundary (step 5f) instead.
  This fixes the precedence inversion where a provider/global _default
  silently overrode an explicit per-endpoint per-model context_length.
- Provider keys accept BOTH id spaces (Hermes id and models.dev id:
  copilot/github-copilot both work) and model ids match
  case-insensitively, mirroring catalog lookup.
- Malformed override values (context_window: '512k') log a one-shot
  warning instead of being silently swallowed.
- DEFAULT_CONFIG comment: removed the false family/dated-snapshot
  inheritance claim, documented the recognized field list, fill-gap
  semantics, and the id-space rule.
- Tests: rewritten for the new contracts (fill-gap invariants,
  dual-id-space keys, sub-dict merge preservation, one-shot warning);
  added a real-config-yaml e2e plumbing test (mutation-checked: fails
  when the config key wiring is broken).
2026-08-14 02:03:54 +05:30
kshitij dafdba324a feat(models): per-model metadata overrides via model_overrides config
Add a unified model_overrides config section that lets users manually
declare context_window, max_output_tokens, capabilities, cost, and
family for any provider+model — winning over models.dev, OpenRouter, and
hardcoded defaults.

Resolution order (first hit wins):
  1. model_overrides.<provider>.<model_id>  (per-provider+model)
  2. model_overrides.<provider>._default    (per-provider default)
  3. model_overrides._default               (global default)
  4. Normal catalog resolution

Key subtlety: an unknown model id (not in the
catalog) derives base metadata from sensible defaults before patching,
so overriding a model the catalog doesn't know yet is the supported
self-unblock path. This is exactly the #84482 scenario (Upstage
solar-pro4/syn-pro wrong context) and the #8731 scenario (custom/local
models with manual capability declaration).

Wired into:
  - get_model_capabilities() — patches capability fields; unknown models
    get safe defaults (tools on, vision/reasoning off) before patching
  - lookup_models_dev_context() — context_window override, checked before
    catalog lookup so it works even for providers not in PROVIDER_TO_MODELS_DEV
  - get_model_info() — merges override dict onto catalog entry (shallow
    merge); for unknown models, the override is the sole source of metadata
  - get_model_context_length() — step 0b in the resolution pipeline,
    before custom_providers (0c) and before any network probe

Config example:
  model_overrides:
    upstage:
      solar-pro4:
        context_window: 524288
      syn-pro:
        context_window: 65536
    custom:my-local-vllm:
      my-llava-model:
        context_window: 8192
        supports_vision: true
        supports_reasoning: false
        supports_tools: true
    _default:
      context_window: 128000

Fixes #8731
Fixes #84482
Refs #47247
2026-08-14 02:03:54 +05:30
kshitij 4fa728b6be fix: follow-up polish for salvaged PR #85512
- Log (debug) instead of silently swallowing capability-lookup failures in
  anthropic_prompt_cache_policy — a swallowed failure would otherwise
  downgrade an explicit prompt_caching: true to (False, False) with zero
  trace. Matches the sibling MoA branch's logger.debug style.
- Use load_config_readonly() for the None-fallback in
  get_custom_provider_model_capability: the helper only reads, and the
  fallback fires on the blank-stub paths (agent init before
  _custom_providers is assigned, MoA/auxiliary destination planning), so
  skip the ~135us defensive deepcopy per call.
- Add route-isolation regression tests at both levels (config helper +
  agent policy): a prompt_caching declaration for one provider route must
  never apply to another route with the same model name. Mutation-checked:
  both tests fail when the URL match is disabled.
2026-08-14 00:59:38 +05:30
fangliquanflq 316f31c9d5 fix(agent): honor prompt caching capabilities for aliases 2026-08-14 00:59:38 +05:30
Teknium c692312704 feat(kanban): GC stale done-task notify subscriptions
Now that subscriptions survive `done` (completion is reversible —
on every 5s notifier tick forever. Add
kanban_db.purge_stale_done_notify_subs(): one DELETE removing subs
whose task has been done with no new events past a retention window
(age = latest task event, falling back to completed_at/created_at, so
any activity exempts the task; a reopened task is exempt by status
alone). The notifier watcher runs it per board once at startup and at
most hourly, re-reading kanban.done_sub_retention_days (config.yaml,
default 30; 0 disables) at each sweep.
2026-08-13 12:21:04 -07:00
Teknium 720f0443a0 chore: release v0.20.1 (2026.8.13) 2026-08-13 11:52:47 -07:00
Botsson e5573a8f8c fix(config): recognize cron script timeout 2026-08-13 11:49:24 -07:00
Greg Gibeau c600fd46bd fix(memory): complete discovery and registration parity for out-of-tree providers
Builds on the three salvaged commits: adds the sources and integration points
they leave out, so a pip-installed memory provider is not a second-class
citizen next to a directory install.

Discovery
- Project-local providers (./.hermes/plugins/<name>/), gated on
  HERMES_ENABLE_PROJECT_PLUGINS exactly as PluginManager gates its own project
  scan. Completes the four sources CONTRIBUTING.md and AGENTS.md already
  promised; memory was the only discovery system missing two of them.
- find_provider_dir() now resolves a package entry point to its directory.
  This is load-bearing: config_schema.py (the dashboard panel) and cli.py (the
  `hermes <provider>` subcommands) are read from disk rather than imported, so
  without a directory a pip-installed provider silently lost both.
- list_memory_provider_names() includes entry-point providers, so they appear
  in the dashboard's memory.provider dropdown.

Resolution stays import-free. hermes_cli.plugins.resolve_module_origin() is
extracted from _resolve_module_source() (added by the salvaged #76567) and
shared, so discovery walks a module's file layout instead of importing it.
find_provider_dir() is called from the dashboard and from argparse setup, long
before the operator has chosen a provider — importing every installed candidate
would execute third-party code on the strength of a package being present.
A test asserts the resolution leaves no side effects and no sys.modules entry.

Registration
- PluginContext gains register_memory_provider(). Memory was the only provider
  category without one; context engine, image gen, video gen, web search,
  browser, TTS, transcription, secret source, dashboard auth and platform all
  have one.
- _ProviderCollector delegates unknown register_* calls to a real
  PluginContext instead of carrying three hand-written no-ops. It silently
  dropped register_tool/register_hook, and had no register_auxiliary_task at
  all — despite PluginContext.register_auxiliary_task documenting a memory
  provider (hindsight's pre-retain dedup) as its worked example. It can no
  longer drift behind PluginContext.
- A raise after register_memory_provider() no longer costs the provider. The
  loader caught it into a debug log, discarded the registered instance, and
  fell through to "instantiate any MemoryProvider subclass" — returning a
  different, unconfigured provider. A silent downgrade that looked like
  success, and the exact outcome of calling register_auxiliary_task.

Activation is unchanged: still gated on memory.provider naming the plugin, and
covered by a test so the real PluginContext cannot start requiring
plugins.enabled — that would break every existing user-installed provider.

Verified end to end against a real third-party provider (kainappsinc/elephant)
installed by pip alone, with no directory copy: it appears in the dropdown,
resolves its directory, loads with its tools, and renders its dashboard panel.

Closes #40101.
2026-08-13 11:49:14 -07:00
Mike Smith a883977b12 test(plugins): activation-contract coverage for entry-point classification
Documents and tests the routing contract the sweeper review asked about:
classification records the manifest but does not activate anything.

- model-provider test now exercises providers.get_provider_profile() against
  the pip-only name (None today — providers discovery is directory-based)
  and asserts the module never leaks into sys.modules via that path.
- new test for the mnemosyne shape: a pip entry point duplicating a
  same-name directory provider. The pip copy is classified exclusive and
  never imported; the directory copy still activates through
  plugins.memory discovery, exactly once.
- _classify_entrypoint_kind docstring now states the activation contract
  explicitly: pip-only providers were equally unactivatable pre-change
  (both destination systems are directory-only; the
  hermes_agent.memory_providers entry-point group has no consumers), so
  classification only removes the wasted import. Entry-point activation
  is tracked upstream (#40644 for memory); this change is its
  prerequisite, preventing double import once it lands.
2026-08-13 11:49:14 -07:00
Mike Smith 450bd0930a fix(plugins): never import parent packages of dotted entry points
find_spec() on a dotted module name imports the parent package first,
executing its __init__.py — which is exactly where a provider's heavy
imports typically live (fastembed -> onnxruntime and friends). The
previous classifier only preserved the no-import property for
top-level entry points.

_resolve_module_source() now resolves only the top-level name with
find_spec() (import-free for top-level names) and walks the remaining
dotted segments through submodule_search_locations by hand, mirroring
PathFinder's file conventions (part.py module / part/__init__.py
package). Namespace packages, zipped modules, extension modules, and
anything else unexpected fall back to standalone (the safe default).
.pyc origins map back to source via source_from_cache.

Regression: a dotted entry point whose parent __init__.py writes an
execution marker and imports the child — asserts the parent never
executed and neither module enters sys.modules during classification.
Fails against the previous implementation (marker written), passes now.
2026-08-13 11:49:14 -07:00
Mike Smith 826e9d18af fix(plugins): classify pip entry-point provider plugins without importing
Entry-point (pip-installed) plugins exposing register_memory_provider()
or register_provider() + ProviderProfile were treated as plain
standalone plugins and eagerly imported by the general PluginManager,
even though memory and model providers have their own discovery
systems and the module has no register() for the general manager to
call. The import registered nothing and paid the module's full import
cost in every Hermes process (a pip memory provider pulls fastembed ->
onnxruntime, ~60 MB RSS).

Entry-point manifests now get the same source-scan classification as
directory plugins via a shared _detect_kind_from_source() helper: the
module is resolved with importlib.util.find_spec (no import) and its
first 8192 chars are scanned for provider markers. Memory providers ->
kind=exclusive, model providers -> kind=model-provider; both are
recorded for introspection and skipped by the general loader.
Unresolvable or non-Python modules stay standalone (default behavior
unchanged).

Tests: an enabled pip entry-point memory provider is never imported;
a pip entry-point model provider routes to providers/ discovery.
2026-08-13 11:49:14 -07:00
brooklyn! 266b2b3611
fix(update): repair failed Node deps on an already-current checkout (#85539)
A failed npm install during `hermes update` prints "Fix npm and re-run
`hermes update`" -- but re-running on a current checkout hit the
"Already up to date!" early return before the Node refresh, so the
repair advice could never work and node_modules stayed stale forever
(#77211).

The commit_count == 0 path now runs the Node refresh through
_repair_node_deps_on_current_checkout. _update_node_dependencies
self-gates on the lockfile hash, which is only recorded after a
SUCCESSFUL npm install (and re-trips when node_modules is missing or
the web toolchain never landed), so healthy installs pay one hash
check and nothing else; a previously failed install actually repairs.
A clean refresh pairs with the web build like every other call site;
a failed one surfaces the fix-npm hint instead of "Already up to
date!".

Fixes #77211.

Co-authored-by: RelaxJonh <RelaxJonh@users.noreply.github.com>
Co-authored-by: JonthanaHanh <JonthanaHanh@users.noreply.github.com>
2026-08-13 18:42:42 +00:00
Teknium b8d9230cf2 feat(models): add google/gemini-3.7-flash to nous + openrouter catalogs, drop gemini-3.6-flash
Swaps the Google flash entry in the OpenRouter and Nous Portal curated
lists to the newly released gemini-3.7-flash (half the price of
3.6-flash: $0.375/M in, $1.875/M out per OpenRouter live metadata;
served on both endpoints, verified live). Also updates the OpenRouter
plugin fallback_models mirror and regenerates model-catalog.json.

Scoped to the two named providers: vertex/gemini/gmi curated lists and
aux defaults still carry 3.6-flash.
2026-08-13 11:28:16 -07:00
embwl0x 5a10537b24 fix(sessions): stabilize legacy reset lineage on resume 2026-08-13 23:45:21 +05:30
Teknium acadd719d3 docs: note Grok 4.6 priority overrides in resolve_fast_mode_overrides docstring
Follow-up to the salvaged #84820; mirrors the doc line from #84848.
2026-08-13 11:07:37 -07:00
fangliquan db5e2402c2 fix(xai): preserve Grok 4.6 wire capabilities 2026-08-13 11:07:37 -07:00
Teknium c495be19aa fix(kanban): inherit ALL routing columns in notify-sub inheritance
_inherit_notify_subs (link_tasks / triage-decompose / create-parents path)
copied only platform/chat/thread/user/profile, dropping chat_type,
user_id_alt, delivery_mode, and delivery_metadata. A DM-originated child
completion then fell back to chat_type='group' and woke a fresh
group-scoped session instead of the originating DM; Telegram DM-topic subs
lost their persisted reply-fallback metadata (issue #73030).

Consolidates the duplicated inline inheritance block in create_task onto
the single-owner helper — one inheritance path, every column, ONE owner.
Sabotage-verified regression tests for both the link_tasks and
create-with-parents paths.
2026-08-13 10:58:30 -07:00
Teknium 6a1103dff2 fix(kanban): default api_server notify subs to notify+wake
api_server is stateless — its adapter has no push send(), so the wake
self-post IS the delivery on that path. Defaulting those subscriptions to
plain 'notify' left them with no delivery mechanism at all (the notifier's
doomed send() failed 12 times then dropped the sub), regressing the
pre-delivery_mode behavior and failing
test_apiserver_sub_wakes_real_session_via_self_post in CI slice 5.
Explicit modes still win; other platforms keep the 'notify' default.
2026-08-13 10:47:40 -07:00
Teknium 0818086c50 fix(kanban): backfill legacy gateway notify subs to notify+wake on first migration
Before delivery_mode existed the notifier woke unconditionally when the task
carried a session_id — pre-existing gateway subscriptions had de facto active
wake. The column's 'notify' default alone would silently disable that on
upgrade. Backfill gateway rows to notify+wake on first-add only (tui stays
notify); explicit user downgrades are never overwritten by re-migration.
Sabotage-verified regression tests included.
2026-08-13 10:47:40 -07:00
verybigdog 6e81ce273c feat(kanban): explicit notify/wake delivery modes with faithful wake session routing
Salvage of #37865 by @verybigdog. Adds delivery_mode (notify / notify+wake / wake)
on kanban notify subscriptions, persists chat_type + user_id_alt so a woken turn
reconstructs the creator's real session key, inherits the return path to child
tasks, and keeps wake out of the model-exposed send_message schema.

Original commits were authored under a local placeholder identity
(hermes-agent@users.noreply.local); re-attributed to the contributor's
public email.
2026-08-13 10:47:40 -07:00
Ben 34c727c5c2 feat(hindsight): memory provider improvements — recall_sync, retain_source, setup templates, memory indicators, error hints
Bundles previously-separate Hindsight/memory PRs into a single review surface:
- opt-in synchronous recall (recall_sync) — recall the injected memory in-turn instead of next-turn prefetch (#5820)
- actionable error when local_embedded runtime is missing — tells the user which package to install (#7718)
- default retain_source to 'hermes' so every stored memory self-identifies its provenance
- offer a starter memory template during hermes memory setup, plus warn before overwriting an already-configured bank
- warn when a configured memory provider reports unavailable (#2765)
- deterministic 'recalled N memories' recall indicator — Hermes itself emits a status line when auto-recall injects memory
- 'saving to memory' retain indicator — emitted the moment a turn is dispatched to the writer

Authored by @benfrank241 (ben.bartholomew@vectorize.io).
Salvaged from PR #74379.
2026-08-13 23:15:25 +05:30
kshitij ace830134e fix: reuse redact_sensitive_text, fix leaky abstraction, fix test data
Follow-up fixes from /hermes-pr-review + /simplify-code on PR #83437:

1. Replace _redact_secrets with agent.redact.redact_sensitive_text(force=True)
   — the plugin's 11-pattern list was a strict subset of the 50+ patterns in
   agent/redact.py. Secrets like Stripe keys, Google API keys, GitLab tokens,
   HuggingFace tokens, DB connection strings, and Telegram bot tokens would
   all leak through the plugin's list but are caught by the existing redactor.
   Added pk-lf- (Langfuse public key) to _PREFIX_PATTERNS in agent/redact.py.

2. Remove dead 'not isinstance(client, object)' check in on_session_finalize —
   always False for any Python value.

3. Fix MoAClient.last_reference_metrics() to call the public
   self.chat.completions.last_reference_metrics() instead of reaching into
   the private _last_reference_metrics attribute via getattr.

4. Deduplicate _coerce_request_messages call in on_pre_llm_request — pass
   pre_coerced=input_messages to _messages_for_langfuse_input to avoid
   double-coercion + double _capture_content serialization per API request.

5. Add HERMES_LANGFUSE_CAPTURE to OPTIONAL_ENV_VARS in hermes_cli/config.py
   for consistency with the other HERMES_LANGFUSE_* env vars.

6. Fix test_sanitized_mode_redacts_secrets test data — the old samples
   ('sk-abc...1234', 'sk-ant...1234', 'Authorization: Bearer ***') were too
   short to match the regex thresholds and never actually tested redaction.
   Updated to realistic-length secrets and changed assertions to check that
   the output differs from input (redact_sensitive_text masks rather than
   inserting the literal string 'REDACTED').
2026-08-13 23:10:16 +05:30
kshitij e665300d6b feat(langfuse): widen tracing to errors, sessions, subagents, and MoA fan-out
Salvaged from PR #83437 by @erosika, with adopted fixes from @bgodlin (#81054),
@aldoeliacim (#82332), @nftpoetrist (#42326), @rodboev (#39653), @FnExpress
(#64292, supersedes #32175 by @db-aeon), @Per0-1 (#61166), @NaMinhyeok (#64797),
and @liuhao1024 (#43130).

Widens the bundled Langfuse plugin from 6 to 11 hooks and fixes two
attribution bugs. Also adopts shutdown/atexit lifecycle fixes and composes
8 prior community PRs with interaction-fix follow-ups.

Model attribution: on_pre_llm_request and on_post_llm_call now prefer the
wire value (request body model, response model) over the agent attribute,
which goes stale after /model switch or provider fallback.

Cost total: both cost paths now send a summed total alongside the per-type
breakdown, since Langfuse does not derive calculatedTotalCost from
cost_details keys. Subscription-included routes send no cost keys at all.

New coverage: api_request_error closes failed generations with ERROR level;
on_session_finalize/on_session_end close dangling traces for tool-only and
interrupted turns; subagent_start/subagent_stop trace delegated children as
spans; MoA advisor fan-out emits one generation per advisor priced at the
advisor's own model.

Capture modes: HERMES_LANGFUSE_CAPTURE=metadata|sanitized|full (default
sanitized). Sanitized mode redacts secret patterns before truncation.

Adopted lifecycle fixes: shutdown client at session finalize when
reason=shutdown (not on session rotation); atexit finalizer ends open root
spans for short-lived processes; root context manager exited to prevent
interpreter-teardown TypeError; TOCTOU on _get_langfuse() fixed with lock;
reasoning_content surfaced in traces; system prompt included in generation
input for Anthropic/Codex/Bedrock; SDK v3 update_trace replaces set_trace_io.

Closes #29482, #43129, #72661.
Supersedes #81054, #82332, #42326, #39653, #64292, #32175, #61166, #64797, #43130.
Partially addresses #67544 (capture modes + secret redaction; user_id remains open).
2026-08-13 23:10:16 +05:30
Victor Kyriazakos 6e76c2698c feat(cron): config-gated agent scheduling in cron context
Cron-spawned agents have the cronjob toolset unconditionally denied, so
scheduled agents cannot create, tune, or remove jobs even when an
operator wants exactly that (reconciler-style jobs that manage a team's
cron table, follow-up one-shots scheduled from within scheduled work).
The denial is loop-prevention policy, not a security boundary: an agent
with the terminal toolset can already shell out to the CLI, so the
workaround exists but skips every limit and accounting layer.

Add cron.allow_agent_scheduling (config.yaml, default false — byte-exact
current behavior). When enabled, only 'cronjob' leaves the cron-context
denylist; 'messaging' and 'clarify' remain denied as interactivity
constraints, and the user-level agent.disabled_toolsets layering is
unchanged, so a user denylist entry still beats the gate. The cronjob
tool description now states the real policy and the quota bounds instead
of a blanket prohibition.
2026-08-13 09:42:39 -07:00
webdevtodayjason c7c687aa4b feat(plugins): rename hook to transform_api_error_classification per #64231 verdict
Applies the batch-disposition SALVAGE conditions from #64231: the hook id
moves to the taxonomy transform-family name, and run-all-then-pick-first
dispatch now logs a runtime warning when a valid-but-losing classification
is skipped (the #64714 skipped-transform rule). Chaining semantics are
stated explicitly at the VALID_HOOKS entry, the dispatch helper docstring,
and the hooks.md catalog row and detail section.
2026-08-13 09:36:02 -07:00
webdevtodayjason e9a29b9bda docs(plugins): point classify_api_error at the hook-taxonomy contract
The dispatch semantics, privacy note, and cold-path property were already
documented at the VALID_HOOKS entry and the dispatch helper; this adds the
explicit reference to the first-valid-wins shape in
docs/plugins/hook-taxonomy.md (landing via #75861) and the cold-path note
on the helper docstring, per the contract review on #64231.
2026-08-13 09:36:02 -07:00
webdevtodayjason a2a99418ee docs(plugins): conform classify_api_error dispatch wording to the mutating-hook taxonomy
The taxonomy write-up for #64231 names the Shape B contract
run-all-then-pick-first: every registered callback runs with failures
isolated, then the first valid result in registration order wins. Align
the hook comment, helper docstring, and hooks.md section with that
wording, add the Privacy flag on error_message/error_body, and state the
cold-path trigger explicitly. Wording only, no behavior change.
2026-08-13 09:36:02 -07:00
webdevtodayjason 0180907fe8 fix(plugins): synthetic hook fixture, shell-hook exclusion, docs per review
Rebased onto current main, where the OpenRouter tool-use 404 is now
handled natively (the bundled demo's exact reason to exist), so the demo
plugin is removed per the standalone-repo policy and every test now uses
a synthetic unclaimed error (fake provider, neutral message, no status
code) that no present or future built-in rule can claim.

classify_api_error is now explicitly Python-plugin-only: VALID_HOOKS
doubles as the shell-hook allow-list, but the shell response parser has
no channel for the classification directive, so shell registrations are
refused at config parse with a warning instead of being silently
ignored (new SHELL_UNSUPPORTED_HOOKS set + regression test).

The hook is documented in the hooks reference as the third
behavior-changing hook, with the full kwargs contract, return shape,
and the Python-only note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
2026-08-13 09:36:02 -07:00
webdevtodayjason 1d93b549ca feat(plugins): add classify_api_error hook so provider plugins can own error quirks
Adds a plugin seam at the top of agent/error_classifier.classify_api_error()
(step 0, before the built-in pipeline) so model-provider plugins can classify
their provider's error quirks without patching core:

- New "classify_api_error" entry in VALID_HOOKS. Callbacks receive the parsed
  error context (provider, model, status_code, error_type, error_code,
  error_message, error_body, error, approx_tokens, context_length,
  num_messages), self-scope on `provider`, and return None to pass or a dict
  {"reason": "<FailoverReason name>", ...optional recovery-hint overrides}.
- get_plugin_error_classification() helper mirrors
  get_pre_tool_call_block_message(): first valid result wins, invalid dicts
  and unknown reasons are skipped, callback exceptions are isolated — a
  broken plugin can never break classification. Zero behavior change when no
  plugin claims the error (all 179 existing classifier tests pass untouched).
- Bundled reference plugin `openrouter-tool-use-404` (opt-in, like all
  bundled standalone plugins) re-implements PR #58451: OpenRouter's
  "No endpoints found that support tool use" 404 carries no
  _MODEL_NOT_FOUND_PATTERNS signal, so it classifies as unknown/retryable
  and the retry loop burns 3-5 attempts on a deterministic rejection.
  The plugin classifies it as model_not_found (retryable=False,
  should_fallback=True) so the fast-fallback path fires immediately —
  demonstrating a waiting core PR converted to a publishable plugin.

Motivation: ~10 open PRs are single-provider error-classification patches
(#58451, #58355, #58502, #58474, #58366, ...). This hook turns that whole
class of contribution into plugin territory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
2026-08-13 09:36:02 -07:00
webdevtodayjason 5e10351683 feat(plugins): kanban worker-lifecycle, task-mutation, and dispatch-tick observers
Implements the remaining observers from RFC #58548 (@thebizfixer),
accepted as the design basis in the #64231 batch disposition:

- on_kanban_worker_spawned: fires in the dispatch loop after spawn_fn
  returns and the worker PID is durably persisted (the RFC timing
  contract), in both the ready and review lanes.
- on_kanban_worker_exited: tick-derived from detect_crashed_workers;
  fires after every reclaim/accounting txn has committed, carrying
  exit_kind / exit_code / outcome / retry_status.
- on_kanban_worker_stale_claim: fires when release_stale_claims
  reclaims a TTL-expired claim; live-PID claim extensions and deferred
  reclaims stay silent.
- on_kanban_task_updated: task-mutation boundary observer carrying
  changed_fields (field names only); fired by assign_task,
  set_model_override, and set_reasoning_effort, and by the dashboard
  plugin API's direct-SQL priority/title/body editors (single and
  bulk) through the new kanban_db.notify_task_updated seam.
- on_kanban_dispatch_tick: re-port of PR #56066 (@laboratoiresonore),
  renamed per the taxonomy and fired strictly AFTER _dispatch_tick_lock
  is released; the sweeper found the original fired inside the lock,
  where a slow subscriber could extend the single-writer critical
  section and stall a sibling dispatcher.

All five are observer-only (return values ignored), fire after the
relevant write txn commits, and short-circuit on has_hook() so nothing
is built when no consumer registers; every fire site is fully
best-effort so a broken plugin can never break dispatch or a task
mutation. No config surface added. Existing plugins and hook payloads
are untouched.

Mutation-boundary scope: every user-facing task-FIELD editor fires
(assignee, priority, title, body, model/provider override, reasoning
effort). Deliberately not wired: status transitions (they belong to
the lifecycle hook family), dispatcher bookkeeping columns
(worker_pid, workspace_path, claim columns — surfaced through the
worker hooks instead), link/comment/attachment tables (not task-row
writes), and the dispatcher's default-assignee auto-assign (already
surfaced via DispatchResult.auto_assigned_default in the tick
payload). notify_task_updated is the seam for wiring further paths.

Docs: new rows plus a detail section in the shipped plugin-hook catalog.
Tests: 30 new (9 worker lifecycle, 8 dispatch tick, 8 task updated,
5 dashboard mutation boundary), including a lock-probe contract test
that fails if the tick hook ever fires inside the dispatch lock.

Refs: RFC #58548, #64231 batch disposition, folds #56066.
2026-08-13 09:35:52 -07:00
Teknium 94be919411 feat: rename Codex OAuth provider label to "ChatGPT or Codex Subscription"
Renames the openai-codex provider's display label across the CLI
(hermes model picker, provider labels), the dashboard OAuth accounts
catalog, and the Desktop onboarding + settings provider pickers.
Slug, aliases, and auth flows are unchanged.
2026-08-13 03:06:08 -07:00
Teknium 7060ac7bed feat(computer-use): provision cua-driver at install time and on toolset enable
Choosing Computer Use should be a config flip, not a hunt for
'hermes computer-use install'. Three provisioning rungs:

- install.sh / install.ps1 pre-install cua-driver (best-effort,
  non-fatal, time-boxed at 660s above the upstream installer's 600s
  lock window; --skip-computer-use / -SkipComputerUse to opt out;
  Termux and unwritable-/Applications skipped cleanly)
- PUT /api/tools/toolsets/{name} (dashboard + desktop toggle) spawns
  the background 'hermes tools post-setup cua_driver' action when the
  toolset is enabled while the binary is missing — previously the
  toggle 'saved' but the tool never appeared in the schema because
  check_computer_use_requirements() couldn't find the binary
- hermes tools interactive flow already installed via
  _toolset_needs_configuration_prompt/_POST_SETUP_INSTALLED (unchanged)

Docs: computer-use.md enabling section rewritten around the new flow;
installation.md documents --skip-computer-use.
2026-08-13 02:44:48 -07:00
Teknium d254ad616f fix(cli): align _build_web_ui's npm closure with hermes update's (ui-tui + web + --include-workspace-root)
_update_node_dependencies() installs the unified closure, but update then
calls _build_web_ui(), whose 'npm ci --workspace web' pass deleted
node_modules and re-reified only the web closure — pruning root
devDependencies and the ui-tui hoisted deps the previous step just
installed, while exiting 0. Since the manifests digest was already
recorded, later no-op updates skipped the repair.

Reported by @andrexibiza in the #44772 final review (P1). Reproduced
E2E: '--workspace web' alone removes typescript-eslint/@eslint/js from
root node_modules; the unified closure restores them.

Guards: ui-tui only named when its manifest exists (prebuilt checkouts),
web-own-lockfile (#42973) and Termux (#38772) paths unchanged.
2026-08-13 02:38:28 -07:00
Zak B. Elep 737e7aa562 fix(cli): protect root devDependencies from hermes update's scoped npm ci
Root package.json still owns devDependencies (the shared ESLint flat
config every workspace's eslint.config.mjs imports) even though
agent-browser and @streamdown/math were already removed from root
dependencies. The scoped `npm ci --workspace ui-tui --workspace web`
prunes them the same way it used to prune those; --include-workspace-root
protects them without reintroducing apps/desktop into the install.
2026-08-13 02:38:28 -07:00
Zak B. Elep 03cdc3b20c fix(browser): harden npx agent-browser resolution
- --ignore-scripts on every real npx agent-browser invocation.
  AGENT_BROWSER_NPX_SPEC is a floating ^0.26.0 range, not an exact
  pin, and none of these sites passed it (unlike install.sh/
  install.ps1's own npm install of the same package). Verified against
  the real CLI: `npx --ignore-scripts --prefer-offline -y
  "agent-browser@^0.26.0" --version` resolves cleanly on npm
  11.19.0/node 26.
- _resolve_npx_bin() now checks the Hermes-managed/extended search
  before a bare ambient PATH lookup, validating each candidate with
  node_tool_runnable before trusting it — a bare PATH-first lookup let
  a broken system npx shadow a healthy managed one with no recovery.
- warm_agent_browser_npx_cache() now runs a credential-scrubbed,
  PATH-propagated environment (matching every other agent-browser
  subprocess spawn) instead of inheriting the full parent environment
  including every provider/gateway credential Hermes holds, and kills
  the whole process tree (not just the top-level npx PID) on timeout
  via the new _kill_process_tree helper, since a surviving descendant
  can otherwise hold a capture pipe open past the nominal deadline.
2026-08-13 02:38:28 -07:00
Zak B. Elep 7cb113d6c8 fix(cli): apply Termux carve-out to doctor --live's npx browser probe
_browser_available()'s npx rung was missing the bare-npx-on-Termux
guard its sibling probes (dep_ensure, nous_subscription) already
apply, so it could report the browser probe available on Termux when
local mode would actually reject the bare npx fallback and fail on
first use.

Also adds argv-level coverage for the two real npx launch sites
(_run_browser_command, _run_chrome_fallback_command) and an
end-to-end test proving _find_agent_browser's lazy-install fallback
and ensure_dependency("browser")'s npx check terminate without
recursion.
2026-08-13 02:38:28 -07:00
Zak B. Elep f4d3592b65 fix(cli): restore managed-node-path and PATHEXT-aware fallback rungs
The tools.browser_tool import-failure fallback in _has_agent_browser
dropped the Windows-installer managed-PATH probe and replaced a
PATHEXT-aware shutil.which lookup with a bare Path.exists() check,
reintroducing the .cmd-shim miss that probe was added to fix.
2026-08-13 02:38:28 -07:00
Zak B. Elep b9cbcc6bf5 fix(cli): teach doctor --live and dep_ensure the npx agent-browser cascade
Both probes only checked PATH and node_modules, so they disagreed with
`hermes doctor` on npx-only installs (#43564): doctor --live reported
the browser probe unavailable, and ensure_dependency("browser") could
shell out to install.sh on installs doctor already reports healthy.
2026-08-13 02:38:28 -07:00
Zak B. Elep fa85964ac1 fix(cli): warm npx cache before hermes update's lockfile-unchanged skip
The warm-up ran after the no-op early return, so it almost never fired
on a plain `hermes update`. It's also a synchronous call that can
block for its timeout on a true cold cache (~11s observed) — print a
status line first so that doesn't look like a silent hang.
2026-08-13 02:38:28 -07:00