Commit Graph

7025 Commits

Author SHA1 Message Date
Teknium
05eecbcd97 chore(memory): remove the bundled openviking provider
OpenViking now ships from Volcengine's own repo through the plugin catalog
(plugin-catalog/openviking.yaml, pinned in #131267). Delete the in-tree copy
and its tests; memory_provider_migration already lists openviking in
_LEFT_CORE, so homes configured for it get the catalog plugin installed on
hermes update / first agent start.

The catalog pin starts openviking-server with the full Hermes environment
(the bundled copy filtered it). The docs now say so and point at
volcengine/OpenViking#5553; start openviking-server yourself to avoid it.
2026-10-06 23:13:01 -07:00
kshitijk4poor
4c84ff744e refactor(providers): LM Studio and Tencent TokenHub get provider profiles; base_url-only agents get snapshot cells
lmstudio and tencent-tokenhub were the last chat-completions providers with no ProviderProfile,
so their requests were built by the legacy flag branch in ChatCompletionsTransport.build_kwargs
(name check for LM Studio, host check for TokenHub). They now have profiles that reproduce the
same wire through build_api_kwargs_extras:

- lmstudio: top-level reasoning_effort from the model's published allowed_options. The agent's
  cached probe is passed to the hook as reasoning_options; auxiliary calls (no probe) keep the
  generic nested reasoning they already sent. base_url is left empty on purpose: a loopback
  base_url would map every 127.0.0.1 / localhost endpoint to LM Studio in URL-to-provider
  inference and turn off local-server detection for Ollama, llama.cpp and vLLM. Model listing
  and the doctor probe stay off so the picker keeps LM Studio's own chat-model probe.
- tencent-tokenhub: top-level reasoning_effort clamped to low/medium/high, omitted when thinking
  is off, high when no level is chosen (default_reasoning_config on main-loop turns). Model
  listing and the doctor probe stay off so the picker keeps the curated Hy list.

The name-based LM Studio branch is removed from the legacy path. The host-based branches stay:
an agent built from a base_url alone (run_agent.py main, the batch runner, library callers)
carries provider="" and still reaches the legacy path. 11 base_url-only snapshot cells now pin
those host-detected requests (OpenRouter, Nous, Kimi, Kimi coding, TokenHub, GitHub Models,
NVIDIA NIM, Qwen portal, a localhost LM Studio port, Ollama, a generic endpoint) so the change
that retires the legacy path can prove it sends the same requests.

Deliberate deltas: model ids prefixed lmstudio: / tencent-tokenhub: are now stripped like every
other profiled provider's; TokenHub auxiliary calls with a reasoning config send top-level
reasoning_effort (the main-loop shape) instead of nested extra_body.reasoning.
2026-10-07 11:27:35 +05:30
Dusk1e
c1b205df68 test(kanban): stats text output shows the review lane
From #29082: board_stats() counts every non-archived status, so the plain-text
`By status:` block must list `review` too, and agree with `stats --json`.
Adapted to the current test module (task enters review via request_review).
2026-10-07 11:19:10 +05:30
kshitijk4poor
650b294151 fix(kanban): make can_move total over task statuses; stats derives from the workflow
- can_move: `archived` is a valid source (restore). The dashboard PATCH accepts
  archived -> triage/todo/ready (measured live), so the manual table gains that
  row and the matrix test now measures it too. A same-column drop returns False
  instead of raising or meaning "allowed".
- `hermes kanban stats` text output iterated its own hardcoded status list,
  which omitted `review`; it now loops over DEFAULT_WORKFLOW.keys().
- Tests: drop three asserts that could not fail (VALID_STATUSES is
  DEFAULT_STATUSES; BOARD_COLUMNS is built from keys()); add one invariant:
  can_move answers for every value tasks.status can hold.
2026-10-07 11:19:10 +05:30
Ben Barclay
c950e2278d refactor(kanban): drop unenforced column traits from the workflow groundwork
Review of #130067: traits described kernel meaning nothing reads (and two
descriptions were incomplete: archived also satisfies a parent gate; the
kernel re-checks parents on ready/review). Same reasoning as the dropped
event model: Phase 0 keeps only data that is consumed or checked against
live code; column semantics are designed with the kernel switch.

Nits: VALID_STATUSES is the frozenset itself; precise tuple/frozenset types.
2026-10-07 11:19:10 +05:30
Ben Barclay
6fe5dfb1cf fix(kanban): drop the partial kernel-event model from the workflow groundwork
Review of #130067: the event edges claimed every block lands in 'blocked',
but _route_block routes to todo / triage / blocked, and ~20 kernel status
writes are conditional (crash/reclaim return, failure breaker, parent
reopen, import). Rather than publish a partial contract, Phase 0 keeps only
what is checked now (columns, traits, manual matrix, derived copies); the
event model is designed with the kernel switch in Phase 1.

Also from review:
- can_move rejects unknown column keys instead of defaulting (archive of a
  bogus key returned True).
- the matrix test only accepts 400/409 as a refusal; 5xx or 422 fails.
2026-10-07 11:19:10 +05:30
Ben Barclay
96db175da7 feat(kanban): one workflow definition for columns, traits and transitions
Groundwork for user-defined columns and workflow per board (#54818).

- hermes_cli/kanban_workflow.py: Column/Workflow + DEFAULT_WORKFLOW, today's
  board written down as data (column order, traits, default kernel-event
  edges, manual move allow-list). Not consulted by the kernel yet.
- VALID_STATUSES, dashboard BOARD_COLUMNS, the kanban_list status enum and
  CLI status icons now derive from it. Fixes drift: the tool enum lacked
  scheduled/review; CLI icons lacked triage/review.
- GET /workflow on the dashboard plugin API.
- Tests pin DEFAULT_WORKFLOW to real behavior: every default event edge vs.
  its kernel verb, and all 64 PATCH src->dst moves vs. can_move.
2026-10-07 11:19:10 +05:30
tank321
9de894da21 fix(stt): local turbo model is selectable in every STT picker (#79629)
faster-whisper accepts `turbo` and the config docs listed it, but the CLI
picker, dashboard and Desktop settings stopped at large-v3, so the only way to
use it was hand-editing config.yaml. All three now read it from the shared
STT_MODEL_CATALOG, along with the install hint and the cloud-model warning.

Salvage of #79671 by @tank321, rebased onto the unified catalog from #133825.
2026-10-06 22:01:38 -07:00
Brooklyn Nicholson
3bbe2a1ce2 fix(copilot): recompute api_mode on fallback and stop stale-mode veto (#46527)
Port of #46542 by @MarkVLK onto current main:

- hermes_cli/runtime_provider.py: _copilot_runtime_api_mode must not veto
  a stale configured codex_responses mode for chat-only Copilot models;
  recompute from the live model capabilities.
- agent/transports/codex.py: request summary:"auto" on the GitHub
  Responses API so reasoning summaries surface.
- agent/agent_init.py: recompute api_mode at init-time fallback so the
  fallback provider/model gets its own mode (and the primary-runtime
  snapshot reflects it).
- tests: regression tests for the stale-mode veto, the GitHub Responses
  summary, and the init-time fallback recompute.

Conflicts resolved against main's refactored fallback ladder
(agent/chat_completion_helpers.py) and its expanded transport tests.

Co-authored-by: Mark Vlcek <markvlcek@gmail.com>
Fixes #46527
2026-10-06 23:54:51 -05:00
kshitijk4poor
63b558deec test(doctor): pin root's reserved blocks in the cron store free-space check
Gate r4 2ab (TOOTHLESS): reverting f56ce2179a left the kept tests green. The
doctor rows now stub shutil.disk_usage and os.statvfs instead of free_bytes,
pin os.geteuid to the user each row names, and a root row (disk_usage 0 B,
statvfs f_bfree 4 GB) must report the store writable; it is red without the
root statvfs branch. The ok row stays so the ok path is asserted where
os.geteuid is absent and the root row skips.
2026-10-07 10:20:18 +05:30
kshitijk4poor
ca9113cef7 test(doctor): give each cron store row its expected issue list
Gate r4 quality: the issues assert was a chained comparison inside a
conditional expression and branched on its parameter. Each parametrize row now
carries the expected issue substrings ([] for the ok row), and one assert checks
the count and that every expected substring is present.
2026-10-07 10:20:18 +05:30
kshitijk4poor
9e80007b58 test(doctor): pin the cron store writability and low-space rows
Gate finding C12 r3 #4 (2ab, toothless): _check_cron_store had no test,
so its warning, its issue and its low-space branch could all break silently.
One parametrized test covers each row with probe_store / free_bytes stubbed:
a writable store reports ok with no issue; a probe error warns "NOT
writable (ENOSPC" and records the fix as an issue; free space below
LOW_FREE_BYTES warns and asks for disk space. It goes red with b57b5a0368
reverted.
2026-10-07 10:20:18 +05:30
Teknium
c905fad241 test(pty): pin the 1:1 marker discard when the disconnect cancels mid-close
2c76125128 moved the legacy PTY path's marker discard into a finally so a
handler cancelled while `bridge.close` is still running in a worker thread
drops it. It shipped without a regression test; the existing disconnect
test only hit that timing intermittently on CI.

The new test makes `bridge.close` slow, waits until it has started, then
leaves the client context (which cancels the handler there) and checks the
marker goes. It fails on the pre-2c76125128f handler and passes on main.
2026-10-06 21:43:55 -07:00
Brooklyn Nicholson
503a6b60e5 test(desktop): a valid Windows SSH lock makes its backend lock-owned
The update sweep (_kill_stale_dashboard_processes via _lock_owned_serve_pids)
now leaves a Windows SSH backend to the code-skew watchdog, as on POSIX.
Pin that flip so it isn't undone as an accident.
2026-10-06 21:52:50 -05:00
John Paul Soliva
2646293a20 fix(desktop): Windows SSH host locks let a superseded backend retire
The owner watchdog (#132219) retires a detached serve --isolated backend once
backend.lock.json names a newer spawn, but it only trusts a lock whose logPath
ends in /<ownershipId>/<nonce>.log. Locks on native Windows SSH hosts are
written by windows_ssh_runtime.write_lock from the Desktop's record, which
carries no logPath, so read_valid_backend_lock returned None there and a
reconnect storm kept stacking backends.

write_lock now stamps the runtime's own log path for the lock's nonce, in
forward-slash form so the suffix check matches. All three Windows lock writes
(the in-mutex spawn record and the two Desktop updates) pass through it, and
the fix ships with the host install, so no Desktop rebuild is needed.

Refs #132034
2026-10-06 21:52:50 -05:00
alt-glitch
53fefae5d7 fix(local-runtime): hermes update from v2026.9.24 no longer crashes importing local_runtime
After the pull, the v2026.9.24 updater lazily imports
hermes_cli.local_runtime.processes (in _subprocess_compat.bounded_probe_run)
from the new checkout. Its process still holds the old
hermes_platform.host.facts. The package __init__ eagerly imported every
submodule, so hardware.py ran `from hermes_platform.host.facts import
gpu_class` (added in 827f4ff1d1) against the old facts module, and the
update died with ImportError. This broke e2e-upgrade (core) on main and
on every PR.

The package-level re-exports now resolve on first access through a module
__getattr__, so importing one submodule runs only that submodule. Callers
of `hermes_cli.local_runtime.<name>` are unchanged.
2026-10-06 20:11:00 -04:00
Teknium
9b38eb1432 test(update): a failed pause-record test no longer orphans its draining gateway
The _DRAINING child traps SIGTERM and then polls for a flag file every 10 ms,
and each test released it only in a finally that begins after its first
assert. Any earlier failure (the "up"/"asked"/"stopping" reads, the updater
spawn) skipped that block: the gateway was reparented to init and spun
forever. Two such orphans from red-on-base runs were still alive on a dev
host three days later; a CI red leaks the same way onto its runner.

_child now records every process it spawns and an autouse fixture SIGKILLs
any survivor once the test is over, so the whole file is covered, including
the updater children (time.sleep(120)) and any test added later.

Sabotaging the "stopping" assert: origin/main leaves 1 draining gateway
alive after the failed test, this change leaves 0; the file passes 42/42.
2026-10-06 16:36:33 -07:00
alt-glitch
65bc6727b4 feat(free-tier): send preview_full_connectors on account creation
HERMES_PREVIEW_FULL_CONNECTORS=true|false makes the free-tier mint send
{"preview_full_connectors": <bool>} in the /api/anonymous/create body, so
the account service can record the cohort on the account and gate
toolkits from it. Any other value (unset included) keeps the body {}.

The variable joins the desktop bundle-env allowlist (release script,
banner writer, channel request decoder, BUILDING.md) so a bundle can
bake it in. It is self-reported and visible in the packaged JS: the
service must treat it as a preference, not proof.
2026-10-06 18:00:58 -04:00
Austin Pickett
85d2e49aac test(model-catalog): pin inclusionai/ling-3.0-flash:free in both picker sources
RED on base: the curated fallback list and the published manifest both omit
the model, so the Desktop picker never offers it even though the CLI accepts
it as model.default (#73686).
2026-10-06 17:23:23 -04:00
Austin Pickett
86df3224e2 fix(pricing): wire kilocode into the pricing dispatcher
Kilo Gateway models showed no prices and no Free badge because
_PRICING_FETCHERS had no kilocode branch, so get_pricing_for_provider
returned {} and _apply_pricing skipped the provider row entirely.

Add _fetch_kilocode_pricing, which reads the public OpenRouter-shaped
catalog at {kilocode profile base_url}/models (KILOCODE_BASE_URL
overrides it — the same endpoint the doctor probe uses, no second
hardcode). The Kilo catalog's explicit isFree flag is authoritative
over the rates (a free router can carry non-zero underlying pricing),
and its -1 rate sentinel (auto-router models, 'rate varies') is
skipped rather than shown as a fabricated price.

Fixes #68838
2026-10-06 17:22:46 -04:00
Austin Pickett
c061068b26 fix(dashboard): authenticate the gateway health probe without leaking the key to /health
The dashboard's _probe_gateway_health() sent no Authorization header, so an
auth-protected gateway (API_SERVER_KEY set) answered 401 to every
/health/detailed poll and logged a warning per probe. Read API_SERVER_KEY at
probe time and send it as a Bearer header — to the authenticated
/health/detailed endpoint only; the public /health fallback never receives
the credential.

Fixes #76051
2026-10-06 17:22:39 -04:00
Austin Pickett
0f0b0a12aa fix(plugins): refuse checkout mutations while the gateway is live
Git plugin update/remove/force-reinstall ran git pull or tree swaps
directly in the installed checkout while the messaging gateway could
still be running with that plugin's callbacks loaded. A callback that
defers a relative import until first invocation then fails with
ModuleNotFoundError even though the command reported success (#70473).

One shared guard (hermes_cli.plugins_cmd._refuse_live_gateway_mutation)
now fails every mutating path BEFORE any git operation when the active
profile's gateway is live:

- plugins update (CLI) and dashboard_update_user_plugin
- plugins remove / rm / uninstall, dashboard_remove_user_plugin and the
  plugins.manage RPC (shared _remove_user_plugin tail)
- plugins install --force (a forced reinstall replaces a loaded
  checkout; a brand-new install is not gated)

The liveness read is the canonical profile-scoped probe
(profiles._check_gateway_running -> resolve_gateway_liveness), never
mutating the profile's identity files; a probe failure fails open so an
uninstallable plugin cannot strand. The message gives the
stop/apply/start recovery sequence; --allow-live-gateway on
update/install/remove is the explicit CLI-idiom override.

Integration test (tests/hermes_cli/test_plugin_live_gateway_guard.py)
spawns a real child holding gateway.lock with a gateway-shaped command
line against a temp HERMES_HOME, so the guard is exercised through the
platform's actual liveness probe — not a mock. Stopped-gateway behaviour
is asserted unchanged.
2026-10-06 17:22:18 -04:00
Teknium
911da42e9e refactor(security): remove the bundled tirith scanner from core
tirith ran `tirith check` on every terminal command, default-on with fail-open, as a
pinned PM binary auto-downloaded at startup. Measured against the shipped 0.4.2 binary
it caught 14/16 attack commands but flagged 12/24 everyday agent commands
(`echo $((6*7))`, `"$VAR/x.sh"`, `gh api | python3`, LAN curls, `.dev` hosts,
`pip install` via analysis_incomplete), and in cron/-q deny mode each of those is a
hard block. It only ever saw terminal strings (execute_code, writes and the browser
bypass it), fail-open meant no protection wherever the binary could not install
(Windows, Termux, x64 musl), and it produced a steady issue/PR stream (fail_open
semantics four times, install loops filling disks, a SIGSEGV hang, session-approval
scoping, third-party promo text on approval cards).

Removed: tools/tirith_security.py, the tirith branches of the command guard (the
is_tirith tuple, session-only "always" downgrade, import-error/fail-open paths,
unattended tirith pass), the CLI and gateway startup installers, the PM package and
lock pins, the nix runtime dep, security.tirith_* defaults, the CLI warning string
and three tirith tips (gaps refilled from the tail so translations survive), plus
the gateway's "no automated assessor" warning that only existed to point at tirith.

Config v50 drops security.tirith_* and enables nothing in its place. Hermes ships
no tirith plugin; if tirith upstream publishes one, it goes through normal catalog
admission under their name.
2026-10-06 14:22:13 -07:00
emozilla
93fb5efc61 feat(local-models): pin Flash Next's downloads to the revisions the catalog describes
Downloads resolved files from main, so a re-upload on the publisher's side would change the bytes
behind the catalog's exact sizes and the pricing measured from them. AssetFile carries a revision
and the download plan resolves each file at it. Flash Next pins unsloth's weights and projector to
38bb39ee and ggml-org's head to 052beea; entries without a revision still follow main.

Co-authored-by: Ryan Hill <38674843+RyanUnderhill@users.noreply.github.com>
2026-10-06 16:08:23 -04:00
emozilla
47703c53e5 fix(local-models): keep older apps on their packaged catalog
Older apps refresh catalog.json from main and ignore fields they do not know. Given this
catalog, main's code recommends Flash Next on an RTX Spark and launches it without its MTP head,
with the 26.8 GiB per-layer table loaded and its QSA compute buffer unpriced. Catalog schema 2
makes those apps skip the refresh and keep the catalog they shipped with; they read schema 2
once they update.
2026-10-06 16:08:23 -04:00
emozilla
c71ff7ebdd feat(local-models): the RTX Spark defaults to the 27B
The RTX Spark ships with the 27B as its default model. Flash Next fits its 128 GB pool and stays
available, but Windows backs GPU memory with commit outside the carve-out, so Flash Next loads
only once the carve-out leaves Windows enough memory to back it: with no page file, an 8 GB
carve-out loads it and a 64 or 96 GB one does not.

recommended_entry consults a product-default table before the derived rule, keyed like the
measured-rate table on platform, effective backend and recognized GPU. The derived rule still
decides when the default is ineligible or cannot run resident, under a Vulkan or CPU engine, and
on the DGX Spark. The Recommended badge reads "The default model for this machine, chosen by its
manufacturer."
2026-10-06 16:08:22 -04:00
emozilla
a53e29f36e fix(local-models): draft Flash Next from ggml-org's MTP head
unsloth's MTP heads mark the MTP block as plain attention (compress ratio
0) beside a QSA trunk. Mainline llama.cpp then builds the k-pool inputs
with nothing reading k_idxs, and the server aborts at load with
GGML_ASSERT(buffer), on b11370 and b11430 alike.

ggml-org publishes a head that loads. On a DGX Spark at 256K and
temperature 1.0 it drafts at 39 tok/s on prose and 32 on code, against
~27.5 without MTP. AssetFile gains a repo so a companion can download
from a repo other than the model's.
2026-10-06 16:07:56 -04:00
emozilla
fbb9453fbb fix(local-runtime): price QSA compute buffers at the launch posture
Qwen4Exp's QSA attention scores the whole window each microbatch. Under
the router's unified KV every slot sees the full window, and b11370
reserves ~46 bytes per microbatch token per window token for each
context, device plus host; an MTP head is a second context. At 256K that
is 5.9 GiB per context at -ub 512 and ~23 GiB at -ub 2048, so the
prefill posture Hermes chose on a 128 GB Spark loaded the target and ran
out of memory creating the MTP context.

plan_launch now prices each posture with its own microbatch and context
count, LaunchPlan carries the profile it priced, and catalog entries
name their architecture so the price holds before download. On the
Spark, Flash Next now runs 256K at -ub 512, priced at 83.1 GiB against
81.5 GiB of measured engine buffers.
2026-10-06 16:07:56 -04:00
emozilla
827f4ff1d1 fix(local-runtime): read lazy tensors on demand on integrated GPUs too
llama.cpp's --lazy-mode auto resolves to off on any device without mmap
support, which covers every integrated GPU (b11370, #28160). On a DGX
Spark, Flash Next's 26.8 GiB per-layer embedding table therefore loaded
in full (27.2 GiB resident) while Hermes priced it as left on disk.
--lazy-mode on keeps it out (0.4 GiB resident) at the same decode speed
and within 2% of the prefill speed on GB10.

The preset now passes lazy-mode = on wherever Hermes prices the table as
on disk, so the engine's per-device default never decides residency.
AMD and Intel integrated GPUs, where reading on demand halved prefill,
keep the engine default and are priced with the table loaded.
2026-10-06 16:07:56 -04:00
emozilla
feaa55d9bb feat(local-models): run Qwen3.8 Flash Next as UD-IQ4_XS with its MTP head
Flash Next's catalog build moves from UD-Q4_K_XL (103.6 GiB, too big for
a 128 GB unified-memory machine) to UD-IQ4_XS: 91.9 GiB on disk, ~60 GiB
loaded once the per-layer embedding table stays on disk. The model has
no MTP layers; unsloth ships the head as its own file, so the catalog
gains an mtp_head companion. It downloads with the model, is priced
before and after download, and only once it is on disk does the preset
add --spec-type draft-mtp with the head as --model-draft (depth 3, from
the vendor recipe). min_engine is b11370, the first pinned build with
Qwen4Exp MTP.

With the projector and head, Flash Next needs ~74 GiB at its native
256K window, so the derived recommendation now picks it on 96 GB
discrete and on 96 and 128 GB unified memory. The catalog test admits
4-bit IQ4 builds as Q4-class, the pane's MTP flag covers a shipped head,
and the Hugging Face browser stops listing mtp-*.gguf heads as models.
2026-10-06 16:07:55 -04:00
emozilla
a6d292d3bb feat(local-runtime): price tensors the engine reads from disk as not loaded
llama.cpp leaves a tensor its architecture marks TENSOR_READ_LAZY in the
file and reads its rows on demand; under the default --lazy-mode auto it
does so for a marked tensor over 4 GiB (b11370: per_layer_token_embd in
qwen4exp and gemma4). Hermes priced those tables as loaded weights, so
Qwen3.8 Flash Next's 26.8 GiB per-layer embedding table counted against
memory it never occupies.

The reader records the candidate tensors per part and applies the
architecture from part 1, so a table in a later split part is found.
profile_from_gguf prices weights without them, and a catalog build
carries lazy_bytes so the same holds before download.
2026-10-06 16:07:55 -04:00
emozilla
e2da92f0ae fix(local-runtime): skip a truncated GGUF header and size a split row by all its parts
A header cut short raised struct.error, which no caller of
read_gguf_header catches (they skip a model on ValueError or OSError),
so one truncated file escaped the whole preset pass. A short read now
raises ValueError.

The status row sized a side-loaded split by its first file, which can
be a metadata stub of a few MB; it now sums every part on disk. The
split fixture writes model metadata into part 1 only, as gguf-split
does: a published Flash Next split carries 67 keys in part 1 and only
its 3 split.* keys in parts 2-3.
2026-10-06 16:07:55 -04:00
Konstantin Khlopkov
48d89901c7 fix(local-runtime): price a split GGUF as the sum of its shards
A split model was priced by its first shard alone, so the physics check
and the residency cap admitted giants the card cannot hold: on a 256 GB
box a 175 GB model priced as 6.7 GiB and the router booted with room for
three residents, two of which then co-resided and OOM'd every session.

Sum the tensor table, the host-side embedding duplicate and the per-block
FFN map across every part on disk; architecture metadata still comes from
the first part. The truncated FFN map also disabled spill placement,
which is the lever that would have relieved the pressure.

A part that cannot be parsed is priced out rather than fatal — refusing an
incomplete split to serve remains staged_in's job.
2026-10-06 16:07:55 -04:00
andyst-dev
2fba6e31bd fix(agent): collapse provider aliases in the fallback-chain dedup
`_entry_identity` compared the fallback entry's provider id as a raw,
lowercased string. A provider reachable under two ids — the canonical
models.dev id (`opencode`) and a legacy rename alias (`opencode-zen`) —
was therefore kept as two separate slots in the fallback chain. That shifts
the chain by one slot and lets an alias slip past the same-backend skip,
so a turn can hit the alias slot, recover from a re-seeded rate-limit pool
against it, and exhaust retries without ever reaching the real paid
fallback.

Normalize the provider id (lazy `normalize_provider` import to avoid
cycles) before computing the entry identity, so aliases collapse onto the
canonical id for dedup. The returned chain still carries each entry's
original provider id — only the identity used for dedup is normalized.

Fixes #85235
2026-10-06 15:00:21 -05:00
Brooklyn Nicholson
ee91fdc84d fix(moa): drop PyYAML from the deletion tests and offset config.py growth
CI has no PyYAML and the repo is ruamel-only: the new deletion tests'
seed/parse helpers now use ruamel.yaml (round-trip) instead of yaml.
hermes_cli/config.py FILE_LINES ratchet: the new _apply_user_moa_presets
moves verbatim into the topical MoA sibling hermes_cli/moa_config.py as
apply_user_moa_presets; both call sites (last-known-good fallback and the
load merge) late-import it, keeping no facade growth.
2026-10-06 14:57:23 -05:00
Tharanee
6ca3d65ae7 fix(moa): preserve strict reads and serialize config updates
Adapt the rebased MoA raw read/merge/save to current fail-closed config reads and hold the existing reentrant config lock across the full update. Preserve concurrent partial writes from non-HTTP callers.

Exercise transient read refusal, HTTP autosave/restart/fallback merging, sparse YAML preservation, and deterministic named/legacy concurrent saves. Shared backup timestamp freshness remains a separately documented upstream limitation.
2026-10-06 14:57:23 -05:00
Tharanee
35f398eb5e fix(moa): preserve legacy preset updates and suppress warnings 2026-10-06 14:57:23 -05:00
Tharanee
d2c96d4ffb fix(moa): keep explicit preset names authoritative on load 2026-10-06 14:57:23 -05:00
Tharanee
4133dfad26 fix(moa): preserve privacy and retained preset metadata 2026-10-06 14:57:23 -05:00
Tharanee
f054b2b9ce fix(moa): persist named preset deletion without losing config 2026-10-06 14:57:23 -05:00
Austin Pickett
d53b1720e6 fix(kanban): refuse to resume a Kanban-owned worker session as an untracked writer
Resuming a dispatcher-spawned Kanban worker session from the ordinary
CLI, /resume, /sessions, oneshot or the Desktop gateway would create a
write-capable writer the board cannot observe (and unblocking the card
can dispatch a competing worker into the same workspace).

- hermes_cli/kanban_resume_guard.py: kanban_resume_refusal() refuses a
  session whose Kanban ownership markers are present; fails OPEN only
  on probe errors. HERMES_KANBAN_TASK (the dispatcher's own env)
  exempts the legitimate dispatcher-driven resume.
- SessionDB.is_kanban_owned_session() probes the transcript for the
  worker markers without loading history.
- Wired into: CLI --resume/-c (falls back to a fresh session id),
  /resume + /sessions (keep current session, print refusal), oneshot
  quiet resume, gateway session.resume (error code 4132).
- locales: cli.resume.kanban_owned in en + 16 languages.
- Two latent F811 redefinitions fixed in moved handler code.

Fixes #68779
2026-10-06 15:15:06 -04:00
Teknium
29caf43e1e fix(stt): every model picker lists the runtime catalog; Mistral STT is selectable again
STT_MODEL_CATALOG moves to tools/transcription_common.py and becomes the one table
behind the `hermes tools` picker, the dashboard selects and the auto-correction
sets. Before this, the dashboard and Desktop offered models for only four providers,
Mistral and xAI had no select, DeepInfra had no model field, and Groq still listed
distil-whisper-large-v3-en, which Groq shut down on 2025-08-23. A config naming it
now remaps to whisper-large-v3-turbo.

Mistral Voxtral STT comes back to the picker and the dashboard provider list. It had
been hidden since the 2026-05-12 mistralai quarantine, and the pin is a verified-clean
2.4.8 again. DeepInfra's picker reads its live STT catalog.
2026-10-06 12:08:49 -07:00
Austin Pickett
e9357f0b5c fix(web): custom endpoint Test requires a JSON models reply
A 200 from /models proved nothing: an SPA catch-all serves 200 + HTML for
every route, and _parse_model_entries is lenient by design, so validation
returned ok:true with an empty model list and the Desktop Test button
showed a green success toast for an unusable endpoint. The newer transport
probe (#93622) only flags 404/405/501, which a catch-all never answers.

When no models parse, classify the answer instead of reporting success:
non-JSON bodies (naming the content type), JSON without a model list, and
a genuinely empty list each get ok:false + reachable:true with a distinct,
actionable message — a warning in the UI, so Save stays possible for hosts
that legitimately expose no model list (#47006).

Fixes #83128
2026-10-06 14:51:20 -04:00
Austin Pickett
a39c2610e5 fix(dashboard): bracket IPv6 URL authorities everywhere hosts compose URLs
An IPv6 literal followed by :port is only a valid URL authority inside
brackets; http://::1:9119 is unparseable by browsers and urllib. Add
hermes_cli/url_utils.format_url_host and use it at every site that
builds an http URL from a (possibly IPv6) bind host: the named-profile
dashboard attach URL, the browser auto-open URL, the startup banner,
the plugin-activation loopback request, the proxy listening banner, and
the gateway-migration listener URLs. Zone identifiers (%eth0) are
percent-encoded per RFC 6874.

Fixes #67367

Co-authored-by: sjungwon03 <sjungwon03@gmail.com>
2026-10-06 14:23:00 -04:00
Teknium
32a79afb27 Merge pull request #132361 from NousResearch/fix/update-commit-point
fix(update): make the git/ZIP swap a single crash-safe commit point
2026-10-06 11:11:39 -07:00
Teknium
e032fdbca1 refactor(telemetry): registry prefixes come from the hub adapters; update reasons use tables
- `skills_hub._registry_prefixes` builds the identifier prefixes from the adapters' own
  SOURCE_IDs and `SkillsShSource._ID_PREFIX_ALIASES`, case-insensitively, so `skills.sh/...`,
  `skils-sh/...` and `OFFICIAL/...` stop reading `unresolved`. A matched source is named by
  `source_id()`, the method the router and the contract test use; two test fakes gain it.
- `update_failure_class`: exception type -> class and fixed stop-reason prefix -> class are
  tables instead of an if-ladder. `Windows gateway recovery failed: ...`, the fixed prefix
  update_completion writes when the post-update gateway resume fails, now reads
  `restart_failed` instead of `other` (prefix only, never the message).
- `mcp.servers.add` records through `mcp_rpc_helpers.record_mcp_add`, dropping the 175-column
  noqa line that kept methods_tools.py at its cap (2011 lines now).
2026-10-06 11:10:43 -07:00
Teknium
d6813a5dd7 fix(telemetry): extension failure classifiers are inert; the contract rejects contradictory install rows
- `_publish_failure_class`, `probe_failure_class` and `exception_failure_class` read a raise
  site's `failure_class` with `inspect.getattr_static` (`tagged_failure_class`), and the MCP
  type walk falls back to `connect_failed`. A third-party exception whose `__getattr__` or
  property raised replaced the user's PluginOperationError with RuntimeError (they run at the
  raise site, collection on or off), and the metric row was dropped; it now reads `exception`.
- `counter_dimensions_are_valid` adds the cross-field rule the per-field enums cannot state:
  failure_class in the kind's own set, `none` exactly on success, registry only on skill rows.
  Pre-split rows (no failure_class/registry) still validate.
- A refused dependency consent returns its reason as `_ConsentRefusal`, a str carrying the
  closed class (`non_interactive`, `deps_declined`, `manifest_invalid`), so publication no
  longer classifies by matching the user-facing copy. The text the user reads is unchanged.
- `extension_registry` imports the hub's `_SOURCE_ID_ALIASES` instead of a hand copy.
- test file: schema read uses utf-8-sig (windows-footguns).
2026-10-06 11:10:43 -07:00
Teknium
d3674513c8 docs(telemetry): the send explainer and Desktop consent window name the fresh-install note
The fresh-install receipt is kept from before the consent answer and counted
after an opt-in, so "data from before you opt in stays on this machine" was
false for it on the CLI send explainer and Desktop's consentWindow line in all
9 locales; only the pre-chat offer named the exception. Both now carry the same
"apart from the fresh-install note (noted on this machine and counted only once
you opt in)" clause. No i18n key changes; the CLI lines stay under 80 columns.
2026-10-06 11:10:43 -07:00
Teknium
ce14639879 fix(telemetry): a partial update with a failed build or restart keeps that failed_stage
update_cmd_fleet._verify_fleet_after_update writes `partial` for a failed build
(`not update_complete`) and for an incomplete restart as well as for a
verification gap. The relabel marked the synthetic verify stage failed for every
`partial`, so failed_stage moved from build/restart to verify and every such run
added a failed hermes.update.stage verify row. Verify is now failed by `partial`
only when no stage mark already failed (a stale/down fleet row still fails it).
The appendix row states the rule.
2026-10-06 11:10:43 -07:00
Teknium
717d8a3e5e fix(install-metrics): count the install note on a consented day; purge receipts on every opt-out
report_pending_installs recorded the fresh-install row at the first start,
usually the opt-in day itself (setup asks inside the install). The sender's
CONSENT_GATE_SQL needs period_start >= opened_at, so that day's package is
never sent: the metric mostly measured re-runs over already-consented homes.

- A receipt is recorded only when collection is on AND (sending is off, or an
  open send consent window opened at or before today 00:00Z); otherwise it is
  kept for the first start on a later day. Tested against the real store,
  real reconcile_send_consent and the real CONSENT_GATE_SQL: kept on the
  opt-in day, recorded the next day, and that package passes the gate.
- Every opt-out answer purges pending_installs AND pending_updates:
  save_consent(enabled=False) (setup, pre-chat offer, dashboard banner,
  `hermes setup telemetry`) and the tui_gateway shared_metrics.set RPC, via
  one shared_metrics_process.purge_pending_receipts used by begin_process too.
- Receipts are claimed by a plain rename, so an unparseable or non-dict
  receipt is deleted instead of sitting forever; a receipt no start counted
  within 7 days is deleted unreported (it would count under a later version).
- With Relay instrumentation off the store "settles" a row without recording
  it; the reporter now leaves the receipts for a start that records, instead
  of latching and deleting them uncounted.
- Docs: the hermes.install.run row states the consented-day rule, the 7-day
  cap, opt-out deletion and the interrupted rules.

Invariant tests red at 6246dd0f69d (5 in test_shared_metrics_install_run,
1 in test_shared_metrics_consent_rpc).
2026-10-06 11:10:43 -07:00