Commit Graph
100 Commits
Author SHA1 Message Date
nesquena-hermes 810414b438 feat(#4325): opt-out toggle to disable transcript virtualization
Adds a Preferences setting 'Virtualize long transcripts' (default ON, opt-out).
When disabled, _currentMessageVirtualWindow returns a full non-virtualized window
so the entire transcript renders at once (also short-circuits the virtual
re-measure loop). Server-persisted (virtualize_transcript bool, default True),
hot-applies via renderMessages on toggle. i18n in all 13 locales.

Pairs with @rodboev's #4328 stream-end anchor scroll fix (cherry-picked into
this branch) to fully resolve #4325.
2026-06-17 01:52:06 +00:00
nesquena-hermes 27062fa0e9 release: stamp v0.51.468 (Release QC — onboarding OAuth single-flight) 2026-06-17 00:27:28 +00:00
nesquena-hermes 46b6d059b0 release: stamp v0.51.466 (Release QA — active transcript reconciles on session events) 2026-06-16 22:36:19 +00:00
nesquena-hermes a1f468674f release: stamp v0.51.465 (Release PZ — workspace null-byte hardening) 2026-06-16 22:20:15 +00:00
nesquena-hermes 34ae806a70 release: stamp v0.51.464 (Release PY — scannable model picker) 2026-06-16 22:10:44 +00:00
nesquena-hermes 62a65a4ff1 review polish: surface plugin-provider check failures at warning (greptile P2) + fix #4253 comment mislabel
Independent review (APPROVED clean) + greptile both flagged one non-blocking nit:
_is_known_model_provider swallowed a transient _is_plugin_model_provider failure
at debug level, so a real plugin-backed provider could briefly vanish from the
picker with no visible log. Promoted to logger.warning(exc_info=True).

Also corrected the endpoint-error-hint comments in ui.js: they cited #4253, which
is actually a cron release (mislabel carried from #4279's body). Comment-only —
no logic change to the browser-verified collapsible-groups code.
2026-06-16 22:09:57 +00:00
nesquena-hermes ea9cd16850 fix(providers): gate credential_pool detection to known model providers (#4324)
Fixes the Photon phantom-providers regression from #4247 (v0.51.461): the
credential-pool detection loop added every auth.json credential_pool key to
detected_providers without checking it names a model provider, so the Photon
iMessage plugin's photon/photon_project/photon_user messaging credentials each
rendered as a phantom provider carrying the full auto-detected model catalog
(251 models on the reporter's machine).

Gated on a new _is_known_model_provider() check (custom:* slug, static
_PROVIDER_DISPLAY/_PROVIDER_MODELS entry, or registered model-provider plugin)
on both the load_pool and raw-dict fallback paths; the group-builder's generic
fallback no longer paints an unrecognized id with the global catalog. Real
pool-keyed providers (copilot, aliased google->gemini) still surface.

Integrates the stronger of two independent same-fix PRs — #4330's implementation
(thorough docstring, try/except-hardened plugin check, preserved #1881 comment,
defensive else branch) + its 8-case test that mocks at the WebUI boundary
(stubs hermes_cli, never imports the bundled agent package that's absent in CI,
exercises both the load_pool and ImportError raw-dict paths) — over #4329's
2-case test. Both rodboev (#4329) and the maintainer agent (#4330) independently
found and fixed the same regression.

Reported by Nic.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-16 21:45:43 +00:00
nesquena-hermesandakrhin 872a8a391e feat(model-picker): collapsible provider groups — collapsed-except-selected, cleaner show-more, de-noised rows
Builds on #4279's collapsible-group foundation and extends it into a complete
provider-group UX:

- Groups start COLLAPSED except the group that owns the selected model, so the
  picker opens scannable instead of as a long flat list. Search auto-expands
  matching groups (and searches hidden overflow models); clearing the search
  restores the collapsed-except-selected view.
- 'Show more' restyled as a distinct '+ Show all N models' expander (muted
  italic, indented, no model-id line) that reveals overflow IN PLACE — the group
  stays open, other groups keep their state, and the view scrolls to the newly
  revealed models instead of resetting to the top. Falls back to a full
  re-render in minimal/headless DOM. Endpoint-error groups (#4253) and the
  expanded group persist correctly across a search round-trip.
- Per-row provider chip suppressed for rows already under their own provider
  heading (removes the repetitive 'provider on every row' noise); kept for
  hoisted/search rows.

Verified end-to-end on desktop + mobile 390px (collapse/expand, search +
overflow-search + clear-reset + no-results, load-more in-place + stay-open +
scroll-to-new, endpoint-error visibility). #3691 driver tests updated for the
collapsible-group structure + de-noised chip. In-place-path CI coverage tracked
in #4331.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-16 21:45:30 +00:00
nesquena-hermesandrodboev c521937887 fix review: fail-closed null-byte handling for workspace paths (#4273)
Gate follow-ups on the converged head, all fail-closed hardening with no
contradiction to the existing security model:
- _as_posix_path rejects embedded null bytes (returns None).
- _safe_resolve catches ValueError (null-byte .resolve()) -> raw path so the
  block gate rejects cleanly.
- _workspace_access_error returns a clean 'Invalid path' validation error on
  stat() ValueError instead of an uncaught 500.
- /api/workspaces/add route preflight resolve() now catches
  (ValueError, OSError, RuntimeError) -> clean 400.

Reconciliation note: an interim gate suggestion to restore ~user/~root
expansion and block /root was REVERTED — the full suite surfaced
tests/test_batch_fixes.py::TestRootWorkspaceUnblocked, which pins the
deliberate #510/#521 decision that /root is NOT blocked (Hermes runs as root
in many deployments, where /root is the legitimate home, allowed via the home
carve-out). Expanding ~root for a non-root deployment would register root's
home, so the PR's original 'leave ~user literal' behavior is the correct,
safer choice. Kept only the null-byte fixes; added a guard test pinning that
/root stays unblocked.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-16 19:40:26 +00:00
nesquena-hermes 32f4e53218 docs(changelog): v0.51.463 — harden remote POSIX workspace trust boundaries (#3664) 2026-06-16 18:58:45 +00:00
nesquena-hermesandrodboev b3e0896356 fix(workspace): harden remote POSIX workspace trust boundaries (#3664) (#4273)
Normalize both the candidate and terminal.cwd before the remote-POSIX
containment check so `{cwd}/../other/...` lexical escapes are rejected, and
restore the blocked-system-root gate BELOW the home-directory allow-list in
both resolve_trusted_workspace() and validate_workspace_to_add() so valid
systemd-homed paths under /var/home/... stay allowed. Also normalize POSIX
shapes at the source (_as_posix_path) so every downstream block probe is
shape-consistent (/etc/../home/user -> /home/user).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-16 18:58:31 +00:00
nesquena-hermes d49e55b0db docs(changelog): v0.51.462 — keep session sidebar visible on slow loads (#4167) 2026-06-16 18:42:28 +00:00
nesquena-hermesandai-ag2026 0b57a7101f fix: keep session sidebar visible on slow loads (#4167)
Sidebar vanished during resilient/slow session loads. Preserve sidebar
fields on resilient loads + add an opt-in api() retry so a slow /api/sessions
shows a retryable error state instead of an empty sidebar.

Maintainer fixes for the 3 review-blocking SILENT regressions:
- Restore read_only / is_read_only / gateway_routing to the sidebar response
  allowlist, so read-only (imported CLI + Claude Code) sessions keep their
  rename/action-menu/swipe suppression and the gateway model label renders.
  (gateway_routing_history intentionally NOT sent — the label reader prefers
  the latest gateway_routing, and the full history would bloat each row.)
- Scope-tag the session cache (active profile + all-profiles flag). When a
  /api/sessions refresh fails right after a profile switch, the catch path now
  clears the stale rows and shows the error state instead of re-rendering the
  PRIOR profile's sessions.

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-06-16 18:42:01 +00:00
nesquena-hermesandakrhin 29d15d7360 fix review: profile-scope _CREDENTIAL_POOL_CACHE + getattr-safe runtime_api_key (#4247)
Codex gate caught a cross-profile leak: _CREDENTIAL_POOL_CACHE was keyed
by provider id alone, so a custom provider configured under profile A made
profile B report has_key=True from a stale cache hit (then 401 at request).
Key the cache by (active-profile auth-store path, provider id) at all
get/set/evict sites via _credential_pool_profile_tag(). Also make the two
new bare _entry.runtime_api_key reads getattr-safe (OAuth-only entries lack
the attr), matching the guard the author already applied at providers.py.
Adds a cross-profile isolation regression test.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-16 18:22:07 +00:00
nesquena-hermes af32f564d1 docs(changelog): v0.51.461 — credential pool fallback for custom providers (#4247) 2026-06-16 17:44:47 +00:00
nesquena-hermesandakrhin b81a92ecd5 fix: credential pool fallback for custom providers (#4247)
Custom/aliased providers with keys in the credential pool (added via
hermes auth add) were reported unconfigured in the WebUI because provider
detection only checked env vars. Add _has_explicit_pool_credentials() that
checks the pool while filtering the ambient gh_cli entry (so Copilot doesn't
falsely show configured on any machine with gh installed), cache-routed to
avoid the cold per-provider load_pool latency, and resolve provider aliases
before load_pool() at every retrieval site so detection and retrieval read
the same pool (glm->zai, github->copilot, etc.). /api/models/live wrapped in
profile scope; getattr-safe runtime_api_key for OAuth-only entries.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-16 17:44:10 +00:00
nesquena-hermes 33f6bf9754 docs(changelog): v0.51.460 — recognize command STT providers in voice probe (#4312) 2026-06-16 17:21:29 +00:00
nesquena-hermesandLLMinem ed7bd05530 fix(voice): recognize command STT providers in capability probe (#4312)
The /api/transcribe/capability probe's resolve_provider() only knew the
built-in STT providers, so an explicit stt.provider mapping to a Hermes
command-type provider was reported unavailable and the browser fell back
unnecessarily. Add command_provider_available(), delegating to the agent's
_resolve_command_stt_provider_config(provider, cfg) as the single source of
truth, consulted as the final fallthrough after the built-in chain (the
agent rejects built-in names internally, so ordering can't shadow a native
provider). Added a clarifying comment that command providers are
intentionally omitted from the auto-detect tuple to match the agent's
_get_provider() precedence.

Co-authored-by: LLMinem <LLMinem@users.noreply.github.com>
2026-06-16 17:21:11 +00:00
nesquena-hermes 42bea392be docs(changelog): v0.51.459 — normalize repair-merged user context (#4089) 2026-06-16 17:08:56 +00:00
nesquena-hermesandstarship-s f4b2cf033b fix(streaming): normalize repair-merged user context (#4089)
When the agent's role-sequence repair concatenates the prior context-tail
user row with the submitted current turn as <stale>\n\n<current>, that
polluted pair was persisted into both the model context and the visible
transcript. Add a _detect_stale_user_merge detector (requires the literal
\n\n repair boundary + exact tail/current match after sentinel stripping)
and funnel context-merge and display-merge through one
_strip_stale_user_merge_from_messages so a single rule governs persistence.
The strip is scoped to the new-turn candidate slice only — historical rows
in the already-committed previous_context prefix are never rewritten.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-16 17:08:21 +00:00
nesquena-hermes ca175beca7 docs(changelog): v0.51.458 — O(D²) context-backfill, prevents server hang (#4314) 2026-06-16 16:56:11 +00:00
nesquena-hermesandpsanger 50a290e419 perf(streaming): O(D²·C)→O(D²) context-backfill loop, prevents server hang (#4314)
The backfill inner loop in _merge_display_messages_after_agent_result
re-ran _message_identity (json.dumps) for every future display row on
every outer iteration and used an O(N) list-slice membership test
(in context_keys[_cursor:]), making it O(D²·C). On long tool-call
sessions this pegged a core, held the GIL, and stalled /api/sessions
5-11s — the WebUI appeared hung. Precompute display keys once and keep
a multiset (count-keyed dict) mirror of context_keys[_cursor:] for O(1)
membership → O(D²).

Salvaged from #4314 (the original PR bundled an unrelated, undescribed
HermesWebUIContext feature). Reimplemented the membership mirror as a
count-keyed dict instead of the PR's set: _message_identity returns
DUPLICATE keys for identical-content turns and None for empty rows, so
a plain set diverges from the original list-slice semantics (verified
over 3M adversarial trials); a multiset including None is exact. Added
a differential regression test guarding the equivalence.

Co-authored-by: psanger <psanger@users.noreply.github.com>
2026-06-16 16:55:42 +00:00
nesquena-hermes 048761a002 docs(changelog): v0.51.457 — gateway watcher non-blocking startup (#4297) 2026-06-16 16:44:31 +00:00
nesquena-hermesandminidarkmimi fb78d396c8 fix: make gateway watcher startup non-blocking with timeout (#4297)
start_watcher() ran synchronously in main() at boot; if
_resolve_watcher_target() blocked (locked state.db, slow profile
resolution, I/O stall) the server never bound its port — a startup
brick. Run it on a daemon thread with a 5s join so boot proceeds even
if watcher init stalls; the poll loop already runs on its own daemon
thread so liveness is unchanged.

Co-authored-by: minidarkmimi <minidarkmimi@users.noreply.github.com>
2026-06-16 16:44:14 +00:00
nesquena-hermesandRod Boev a875bcbb9e feat(ux): enlarge Mermaid diagrams in chat (#4075) + workspace CSV table preview (#4025)
Combined light-UX release NW. Both rebased onto current master (were 96 and
247 behind; net 3-dot diffs applied, byte-identical to PR heads). Both reuse
existing renderers (chat image lightbox / chat CSV table renderer).

Review fix (#4025, Codex): the CSV table preview previously stripped the
workspace Edit affordance .csv had when it fell through to the code preview.
Restored: csv mode is editable, renderCsvPreviewContent caches raw content for
the edit textarea, and saving an edited CSV re-renders the table (not markdown).
Verified the full edit round-trip live + regression test.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-14 06:12:24 +00:00
nesquena-hermes cd5a6de231 fix(transparent-stream): keep live prose chronological, don't fold into top Worklog rail (#4096)
In Transparent Stream mode a live multi-round turn (narrate -> tools ->
narrate -> tools) rendered all assistant prose bunched at the top with
every tool/thinking row clustered below; it self-healed only once the
turn settled.

_syncLiveWorklogReasonsForAnchor() runs on every live segment render
(_flushPendingSegmentRender + the RAF _doRender in messages.js) and
unconditionally built the top-anchored live-worklog rail, mirrored each
round's prose into a wl-reason row there, and hid the real inline
assistant-segment via assistant-segment-worklog-source (display:none).
That prose-folding is the Compact Worklog presentation (#3401) and must
not run in Transparent Stream mode.

Gate the helper on isCompactWorklogMode() so transparent mode keeps prose
as visible, chronologically-placed inline segments interleaved with tool
rows (matching the already-correct settled render). Compact Worklog
folding is unchanged. Verified in an isolated mock-LLM env via an
on-screen visual-position probe (before: textAfterFirstTool=0,
allTextAboveAllTools=true; after: textAfterFirstTool=3,
allTextAboveAllTools=false) and confirmed compact_worklog still folds.

Adds a behavioral regression test asserting the gate is the first
statement in the helper (precedes the rail build), red/green validated.
2026-06-14 02:18:33 +00:00
nesquena-hermes 39e9790597 Release v0.51.366 — Release ME (assistant turn anchor source normalizer Slice 2, #3980) 2026-06-12 06:45:02 +00:00
nesquena-hermesandrodboev b26bb559d5 feat(ux): surface memory/skill saves in Activity summary (#3544)
Absorbs contributor PR #3544 (@rodboev, closes #3340) with two fixes:

1. DETECTION VOCAB (would never fire): the original gated on action names
   {save,create,update,upsert}, which don't match the real agent tool enums —
   memory.action is add|replace|remove, skill_manage.action is
   create|patch|edit|delete|write_file|remove_file. Split into per-tool
   predicates with the correct vocabularies: _isMemorySave gates memory on
   {add,replace}; _isSkillUpdate gates skill_manage on {create,patch,edit,
   write_file}. Deletions excluded so the saved/updated verbs stay accurate;
   running/errored excluded.

2. SNAPSHOT/RESTORE PERSISTENCE (Codex catch): classification lived only on the
   row._tcData JS property, which does NOT survive the outerHTML/innerHTML
   snapshot+restore the live tool-call group uses on session switch/restore —
   a restored memory/skill row would be re-counted as a generic tool and the
   suffix would silently vanish. buildToolCard now also stamps durable
   data-memory-save / data-skill-update attributes, and _syncToolCallGroupSummary
   counts them as a fallback when _tcData is absent. Verified live across a real
   outerHTML round-trip: label identical before/after.

Replaces the PR's static source assertions with a node-driven behavioral test
(11 cases) covering the real action vocabularies, exclusions, case-insensitivity,
null-arg safety, and the durable-attribute persistence guard.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-05 23:09:00 +00:00
nesquena-hermesandrodboev 10413b4d65 Release v0.51.281 — Release IW (stage-verdigris — Verdigris emerald/bronze skin #3602)
Adds the Verdigris dark-only appearance skin (emerald/forest-green + bronze-gold),
renamed from the contributor's 'Hermes Agent' to a descriptive material name per
maintainer naming convention. Registered across all 5 skin sites (config allowlist,
boot.js swatch, index.html FOUC map, i18n in 12 locales, scoped CSS palette) + test.
Also fixes the zeus i18n test (zeus is no longer the trailing skin token).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-05 22:18:47 +00:00
nesquena-hermes c9dfa33e39 Stamp CHANGELOG for v0.51.118 (Release CP / stage-pr2773 / 1-PR hotfix) 2026-05-22 23:22:27 +00:00
nesquena-hermes d8b82e274a docs: stamp PR #2773 in CHANGELOG entry 2026-05-22 23:17:47 +00:00
nesquena-hermes 12becd1f4b fix(chat): rename _inflightStateLimits() to _getInflightStateLimits() to fix v0.51.117 collision
Closes #2771.

v0.51.117 (PR #2766) introduced a top-level function _inflightStateLimits()
in static/ui.js that collided with the window._inflightStateLimits config
object set in static/boot.js. Because top-level function declarations in
classic (non-module) scripts attach to window, boot.js's assignment
overwrote the function reference, and every later _inflightStateLimits()
call threw TypeError. _compactInflightState() runs on every send(), so
no new chat session could be created — v0.51.117 is effectively unusable.

Reported by @jahilldev, with multiple users (@isma3iloiso, @theDanielJLewis,
@JHVenn) confirming the bug or reverting to v0.51.116.

Fix: rename the function to _getInflightStateLimits() — the window-attached
config key stays under its original name (unchanged for any downstream
code that reads it). Updates all 4 call sites in static/ui.js.

Tests:

  - Update tests/test_inflight_storage_quota.py — the existing test
    asserted 'function _inflightStateLimits()' in UI_JS as a positive
    presence check, which certified the bug. Now asserts the renamed
    function name is present AND the old colliding name is absent AND
    no stale call sites remain.
  - Add tests/test_window_function_collision.py — generalized regression
    that scans every static JS file for top-level function declarations
    whose name also appears as the target of 'window.X = {...}' or
    'window.X = <number>'. This is the exact shape that broke #2715
    (_pinnedSessionsLimit in v0.51.106) and #2771. Test fails with a
    precise diagnostic naming the file and symbol if the bug class
    returns. Confirmed test FAILS on current master (unfixed) and PASSES
    on this branch.

Verified end-to-end against the live browser before commit:
  - typeof window._inflightStateLimits === 'object' (config preserved)
  - typeof window._getInflightStateLimits === 'function'
  - _getInflightStateLimits() returns the limits object
  - saveInflightState() persists to localStorage without throwing

Full pytest suite: 6308 passed, 6 skipped, 3 xpassed, 8 subtests passed.
Opus advisor: SHIP.
2026-05-22 23:17:00 +00:00
nesquena-hermes 15afabf849 docs: stamp PR #2758 in CHANGELOG entry 2026-05-22 17:25:54 +00:00
nesquena-hermes 0703a07654 fix(updates): pass --force to git fetch --tags to recover from remote re-tags
Without --force, git fetch origin --tags refuses to overwrite divergent
local tags and returns 'would clobber existing tag', jamming the entire
WebUI update path indefinitely. The WebUI is a release-tracking consumer
that never pushes tags, so it should always defer to whatever the remote
says a release tag points to. Add --force to all three fetch-tag call
sites:

  - _check_repo (the 'Check now' button + periodic check)
  - apply_force_update (force-reset to remote HEAD)
  - apply_update (stash + pull --ff-only)

Tests:

  - Updated 3 existing tests in test_updates.py whose fake_git mocks
    asserted the exact ['fetch', 'origin', '--tags'] args list.
  - Updated 1 existing test in test_update_banner_fixes.py that asserted
    the same shape for apply_update.
  - Added 4 new regression tests:
      - test_check_repo_fetches_tags_with_force
      - test_apply_force_update_fetches_tags_with_force
      - test_apply_update_fetches_tags_with_force
      - test_check_repo_recovers_from_remote_retag (end-to-end,
        proves the bare --tags fetch shape is no longer used)

Closes #2756.
2026-05-22 17:25:54 +00:00
nesquena-hermes 403e9582af Stamp CHANGELOG for v0.51.82 (Release BF / stage-375) 2026-05-17 03:53:50 +00:00
nesquena-hermes 573b1e12ad Stage 375: PR #2428 stage-fix #2 — drop angle brackets from pipe-protect set (Opus catch)
Opus advisor review of stage-375 flagged that the protected-bracket set including `<` and `>` caused tables containing comparison operators across adjacent columns to mis-collapse: `| x < 5 | y > 10 |` matched `< ... >` as a bracket pair and stashed the inner pipe, producing one cell instead of two.

Real LLM table output uses angle brackets as comparison operators far more often than as content-grouping pairs, so the safer default is to NOT treat them as a matched pair. Dropped `<` from the opener class and `>` from both closer classes.

Three regression tests added (`TestComparisonOperatorsAcrossColumns` class): `< … >` across columns, `<` alone, `>` alone.
2026-05-17 03:53:29 +00:00
nesquena-hermes d45d4c08b5 Stage 375: PR #2428 stage-fix — hex-escape brace literals in pipe-protect regex + drop apostrophe-stop + add 9 regression tests
PR #2428's iterative _protectPipes regex introduced two issues we caught during stage assembly:

1. The negated character classes [^)\]}'>] added `'` as a stop character. That breaks cells containing string-literal pipes like `('a'|'b')` (Python type-union examples) — they would still mis-split. Dropped the apostrophe-stop.

2. The literal `}` inside the regex character classes confused the brace-counting extractFunc driver in tests/test_renderer_js_behaviour.py, breaking all 45 existing node-driven renderer tests. Rewrote both brace literals as hex escapes (\\x7b and \\x7d) — semantically identical at the regex-engine level but the JS source carries no bare brace glyph.

Also added tests/test_issue2428_table_pipe_protection.py with 9 regression tests covering single-pipe, multi-pipe-in-brackets, apostrophes-with-pipes, and the KaTeX \$...\$ guard.
2026-05-17 03:41:43 +00:00
nesquena-hermesandMichael Lam e9c6b7f06c Stage 375: PR #2432 — feat(theme): add Catppuccin appearance skin (Latte + Mocha palettes) by @Michaelyklam (closes #2426)
Co-authored-by: Michael Lam <michael@example.local>
2026-05-17 03:35:19 +00:00
nesquena-hermesandmanji 1793aa8c85 Stage 375: PR #2428 — fix(renderer): protect pipes inside brackets from table cell split + KaTeX dollar in tables by @bengdan
Co-authored-by: manji <manji@local>
2026-05-17 03:35:19 +00:00
nesquena-hermes c7365548b7 Stamp CHANGELOG for v0.51.81 (Release BE / stage-374) 2026-05-17 03:10:09 +00:00
nesquena-hermes b78c739662 Stage 374: assemble unified CHANGELOG for v0.51.81 2026-05-17 02:50:19 +00:00
nesquena-hermesandMichael Lam 9fec727cf7 Stage 374: PR #2424 — feat(runtime): add default-off RuntimeAdapter seam (HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal) by @Michaelyklam (refs #1925)
Co-authored-by: Michael Lam <michael@example.local>
2026-05-17 02:49:35 +00:00
nesquena-hermesandFrank Song 8f98465024 Stage 374: PR #2427 — fix(streaming): recover journaled partial assistant output after WebUI restart by @franksong2702 (fixes #2423)
Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-05-17 02:49:35 +00:00
nesquena-hermesandMichael Lam a33cd4195b Stage 374: PR #2431 — fix(chat): render pending assistant placeholder before /api/chat/start returns by @Michaelyklam (fixes #2429)
Co-authored-by: Michael Lam <michael@example.local>
2026-05-17 02:49:35 +00:00
nesquena-hermesandBonyFish 13265b7085 Stage 374: PR #2425 — i18n: add internationalization for Settings Plugins panel by @mccxj
Co-authored-by: BonyFish <mc02cxj@gmail.com>
2026-05-17 02:49:34 +00:00
nesquena-hermesandMichael Lam 47c210899e Stage 374: PR #2421 — fix(cache-tokens): surface provider prompt-cache read/write tokens in WebUI usage by @Michaelyklam (fixes #2419)
Co-authored-by: Michael Lam <michael@example.local>
2026-05-17 02:49:34 +00:00
nesquena-hermesandMichael Lam b3bf2347e4 Stage 374: PR #2418 — fix(cost-history): provider-specific POSIX file lock for snapshot updates by @Michaelyklam (fixes #2402)
Co-authored-by: Michael Lam <michael@example.local>
2026-05-17 02:49:15 +00:00
nesquena-hermes ae83b61185 Stamp CHANGELOG for v0.51.80 (Release BD / stage-373) 2026-05-17 00:23:04 +00:00
nesquena-hermesandFrank Song 8a950cfbdd Stage 373: PR #2417 — fix(streaming): stale compaction task resume on fresh greetings (closes #2308, supersedes #2309)
Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-05-17 00:22:22 +00:00
nesquena-hermes 54f1a2acae Stage 373: PR #2415 — fix: ignore provider config flags in model picker by @Michaelyklam (fixes #2399) 2026-05-17 00:21:50 +00:00
nesquena-hermes 0baefcd1e5 Stamp CHANGELOG for v0.51.79 (Release BC / stage-372) 2026-05-16 23:05:53 +00:00
nesquena-hermes 3480e75e13 Stage 372: PR #2413 — feat(quota-chip): add Settings toggle, flip default to off 2026-05-16 23:05:09 +00:00
nesquena-hermes 780bddca04 Stage 372: PR #2411 — fix: preserve named custom provider model selections by @Michaelyklam (fixes #2405) 2026-05-16 23:04:53 +00:00
nesquena-hermes 862d32ce27 Stage 372: PR #2408 — fix: split live Activity groups at auto-compression by @Michaelyklam (fixes #2404) 2026-05-16 23:04:18 +00:00
nesquena-hermes 9abf17b4f4 Stage 372: PR #2406 — fix: sanitize sync chat history with config by @Michaelyklam (fixes #2398) 2026-05-16 23:03:38 +00:00
nesquena-hermes 55f2d6792b Stage 372: PR #2407 — docs: advance runtime adapter gate by @Michaelyklam 2026-05-16 23:02:42 +00:00
nesquena-hermes cf23d47e2d fix(stage-371): apply Opus SHOULD-FIX — KaTeX/CSV/diff stay LTR + chip 1px boundary
Opus advisor on stage-371 caught three issues during pre-release review:

1. RTL salvage missed KaTeX math (display equations + inline LaTeX), diff
   blocks, CSV tables (column order must read left-to-right regardless of
   chat direction), and .skill-file-path. The first salvage commit only
   covered pre/code/kbd/samp/tt and tool-call bodies. Added a second
   force-LTR block covering: .katex, .katex-block, .katex-display,
   .katex-html, .katex-inline, .diff-block (+children), .csv-table-wrap,
   .csv-table (+children), .skill-file-path. Severity: KaTeX is the most
   user-visible gap — any user rendering math under RTL would see flipped
   equations.

2. Quota chip @media (max-width:1400px) hide rule conflicted at exactly
   1400px with the existing @media (min-width:1400px) .messages-inner
   rule — chip was hidden AT the wide-desktop boundary where it should
   first appear. Changed to (max-width:1399.98px). Visually verified at
   1400px: chip now correctly visible there.

3. Dead .icon-btn.provider-quota-chip selector — chip never has icon-btn
   class. Removed.

Test added: test_rtl_math_and_tables_stay_ltr (pins the 4 new LTR
surfaces). Also removed dead code in test_rtl_code_blocks_stay_ltr
(unused code_block variable).

Per stage-fix protocol: SHIP-with-followup applied on the stage rather
than the source PR, since #2409 is already merged-into-stage and
nesquena-approved. Stage-371 review-bypass batch path still holds.
2026-05-16 22:22:38 +00:00
nesquena-hermes e3eda7e3df Stamp CHANGELOG for v0.51.78 (Release BB / stage-371) 2026-05-16 22:07:43 +00:00
a4ab7d4d27 Stage 371: PR #2409 — Stuck-PR sweep: salvage RTL chat from #1721 + override quota chip from #2082 by @malulian and @ai-ag2026
Co-authored-by: malulian <malulian@users.noreply.github.com>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-16 22:04:56 +00:00
nesquena-hermes 7192d856af docs: refresh CONTRIBUTORS.md and README to v0.51.58
Last refresh was at v0.51.44 (130 contributors / 568 PR credits). Since then,
14 releases shipped (v0.51.45 through v0.51.58) with 78 attributed PRs from
20 distinct contributors, adding 7 first-time contributors:

- @lucasrc — auth-hardening trilogy (#2191/#2192/#2193) → sustained 3-PR bucket
- @LumenYoung — stale-stream guard + 3 streaming-correctness fixes → sustained 4-PR bucket
- @MrFant — 2 PRs (reasoning_content whitelist, message preservation)
- @xz-dev — 2 PRs (thinking-card state, session-scoped metering)
- @legeantbleu — French (fr) locale (#2142)
- @ayushere — ctl.sh bash 3.2 macOS compat (#2117)
- @plerohellec — single PR

Bucket promotions (existing contributors who moved up):

- @dobby-d-elf: 2 → 6 PRs (jumps from two-PR list into top contributors)
- @samuelgudi: 1 → 2
- @michael-dg: 1 → 2
- @vcavichini: 1 → 2
- @hualong1009: 1 → 2

Top contributor PR-count updates (top 7 changed materially):

| Contributor       | Before | After | Delta | Latest release |
|-------------------|-------:|------:|------:|---------------|
| @franksong2702    |     92 |   117 |   +25 | Unreleased    |
| @Michaelyklam     |     81 |    92 |   +11 | v0.51.57      |
| @bergeouss        |     61 |    62 |    +1 | v0.51.46      |
| @ai-ag2026        |     49 |    55 |    +6 | v0.51.47      |
| @dso2ng           |     21 |    23 |    +2 | v0.51.51      |
| @jasonjcwu        |     13 |    16 |    +3 | v0.51.55      |
| @Jordan-SkyLF     |      6 |    12 |    +6 | Unreleased    |

Header refreshed: 130 → 137 contributors, 568 → 646 PR credits, refresh
tag bumped to v0.51.58. Notable contributions paragraphs in both files
updated with the new work (manual /compress async pair, worktree status +
guarded remove, fork-from-here, opencode-go resolver, the LumenYoung +
lucasrc + dobby-d-elf entries).

Source of truth: CHANGELOG.md attribution lines `**PR #N** by @user` between
the v0.51.44 header and the Unreleased section, plus baseline buckets in
the previous CONTRIBUTORS.md. Cross-bucket sanity check passes (no handle
appears in multiple buckets; 21 + 13 + 19 + 84 = 137 unique handles).
2026-05-14 02:18:57 +00:00
nesquena-hermes 9b1d786459 fix(config): preserve nvidia/ prefix on NVIDIA NIM (closes #2177)
Move the `_PORTAL_PROVIDERS` guard in `resolve_model_provider()` to run
BEFORE the `prefix == config_provider` strip branch. The guard was added
for NVIDIA (along with the Nous portal cases in #854 / #894) but was
placed after the strip, so it never fired when `config_provider == "nvidia"`
and the model id started with `nvidia/`.

For `model_id="nvidia/nemotron-3-super-120b-a12b"`,
`config_provider="nvidia"`:
  - prefix = "nvidia", bare = "nemotron-3-super-120b-a12b"
  - prefix == config_provider → True → strip branch returned bare name
  - `_PORTAL_PROVIDERS` guard never reached
  - bare "nemotron-3-super-120b-a12b" sent to NVIDIA NIM → HTTP 404

NIM requires the full namespaced path. The fix moves the portal guard
to run first, so all portal providers (Nous, OpenCode-Zen, OpenCode-Go,
NVIDIA NIM) always preserve the full `provider/model` id regardless of
whether the prefix happens to equal the provider name.

This also closes a latent symmetric bug for the Nous case if a
`nous/<model>` id ever existed in the catalog.

Test plan:
- New `tests/test_issue2177_nvidia_prefix_preservation.py` covers:
  - nvidia/nemotron-... under nvidia (the reported case)
  - cross-namespace qwen/ and meta/ under nvidia (regression pin)
  - every static nvidia model in `_PROVIDER_MODELS` resolves to itself
  - latent nous/<model> under nous (structural ordering pin)
  - non-portal providers (anthropic) still strip — fix doesn't over-correct
- Existing portal-routing suites (test_nous_portal_routing.py,
  test_issue895_894_nous_prefix.py) continue to pass.
- Full test suite: 5320 passed, 4 skipped, 3 xpassed.

Reported on Discord by @vishnu (Nathan forwarded as #2177).
2026-05-13 07:05:57 +00:00
nesquena-hermes d75b59135a stage-341: apply Opus SHOULD-FIX (it i18n + short-circuit logger.debug + docstring)
Opus advisor pass on stage-341 found three surgical items:

1. static/i18n.js:it — PR #2064 branched before stage-340 landed the 'it'
   locale (#2067), missing 9 session_*worktree* keys. Mechanical mirror of
   en/ja position. Italian falls back to English silently without this fix.
2. api/streaming.py — PR #2107's new break short-circuit was silent in both
   the aux and agent title-generation paths. Added logger.debug calls before
   each break so production logs surface the exit shape.
3. api/streaming.py — Expanded _title_should_skip_remaining_attempts docstring
   to document the membership criterion explicitly (vs the implicit
   reasoning-only-burn case it ships with today). Future additions
   (llm_safety_blocked, llm_oauth_quota) have a clear inclusion test.

CHANGELOG updated under the Stage-341 maintainer fixes section to mirror
the stage-340 pattern. All targeted tests pass (57/57 in the affected
modules).
2026-05-12 00:16:33 +00:00
nesquena-hermes 6947970e8d docs: CHANGELOG stage-341 — close v0.51.47, open stage-341 Unreleased
Renames the [Unreleased] section to [v0.51.47] (Release W, shipped today
via stage-340) and folds in the stage-341 batch — PR #2105 RFC, PR #2107
title-retry fix, PR #2064 worktree archive copy, plus the stage-341
maintainer fix (RFC conventions guidance).

Also removes the duplicate v0.51.46 heading line that landed in v0.51.47's
stage-340 merge (the duplicate was a no-op — empty body line under the
extra heading — but tidying it up here.
2026-05-12 00:07:25 +00:00
nesquena-hermes 43677b046d docs(rfcs): add anti-speculative-implementation conventions guidance
When merging PR #2105 (Hermes Run Adapter RFC) the standing concern was
that landing the RFC unconfirmed would invite the speculative-fragment
implementation pattern we just had to put on hold with PR #2071 — well-
written 651-LOC standalone scripts with no callers.

Add a single bullet to the conventions block so the contract is explicit:
an RFC is a design direction, not an invitation to PR fragments against
it. Implementation slices need maintainer confirmation first.

Applied during stage-341 build, not requested from @Michaelyklam — the
guardrail belongs in the conventions doc itself rather than as a one-off
ask on this PR.
2026-05-12 00:05:28 +00:00
nesquena-hermes e20eb2c784 fix: skip budget-doubling title retry for reasoning-only responses (#2083)
Reasoning models (Qwen3-thinking via LM Studio, DeepSeek-R1, Kimi-K2,
etc.) can burn their entire output budget on hidden reasoning tokens and
emit no visible content. The previous title-generation retry path
classified that as llm_length and doubled the budget — but the second
call produces the same shape, so the retry only doubled the GPU/credit
burn. Repeated across the two prompts in _title_prompts() this came to
~3000 reasoning tokens of GPU work per new chat. On local LM Studio
servers behind a custom: provider (where is_lmstudio=False means
reasoning_effort: none never reaches the model) it manifested as the GPU
never going idle after a prompt.

Fix:
  - _extract_title_response: classify reasoning-bearing empty responses
    as llm_empty_reasoning regardless of finish_reason. The presence of
    reasoning_content is the diagnostic signal, not finish_reason.
  - _title_retry_status: drop llm_empty_reasoning from the retry set.
    Length-truncated responses WITHOUT reasoning still retry (those are
    legitimately recoverable by a larger budget).
  - Add _title_should_skip_remaining_attempts() and break out of the
    prompt-iteration loop on empty-reasoning. A second prompt against
    the same model would produce the same shape.
  - Falls through to _fallback_title_from_exchange for a local-summary
    title.

Tests updated to invert the previous reasoning-retry assertions:
  - test_aux_short_circuits_on_empty_reasoning_without_retrying
  - test_aux_still_retries_finish_length_without_reasoning
  - test_agent_route_short_circuits_on_empty_reasoning_without_retrying
  - test_agent_route_still_retries_finish_length_without_reasoning

Companion agent-side work (LM Studio classifier for custom: providers)
is tracked separately on the hermes-agent side; this WebUI fix is the
belt-and-braces guard so the loop stops regardless of agent classifier
state.

Reported by @darkopetrovic. Closes #2083.

Co-authored-by: darkopetrovic <darkopetrovic@users.noreply.github.com>
(cherry picked from commit efeae4a86e377069c0f09d140429ecb111a8dd1a)
2026-05-12 00:04:11 +00:00
84e110db89 i18n(it): complete cron_toast_notifications_* keys
Opus SHOULD-FIX from stage-340 review. PR #2067 added the it locale
between en and ja; PR #2100 added 4 toast keys to 8 other locales but
missed it. Falls back to English via t() defaults so no user-visible
break, but it's an i18n parity hole.

4 LOC, mechanical add inside the it: block at the canonical position
(immediately after cron_profile_server_default_hint, mirroring en/ja).

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Co-authored-by: Samuel Gudi <samuel.gudi.official@gmail.com>
2026-05-11 23:24:42 +00:00
nesquena-hermes 4c2c1190db docs: CHANGELOG Unreleased — stage-340 (4-PR contributor batch)
Italian locale + per-cron toast toggle + stale-gateway agent-health
fix + CI/console hygiene. One stage-340 test patch noted.

PRs: #2100 #2075 #2070 #2067.
2026-05-11 23:14:57 +00:00
nesquena-hermesandSamuel Gudi 02ecc5aeea fix(tests): provide LOCALES on TestVoiceModePreferenceGate
PR #2067 made TestVoiceModePreferenceGate.test_settings_pane_has_voice_mode_i18n_keys
adaptive via self.LOCALES but only defined LOCALES on the sibling class
TestComposerVoiceButtonI18n. AttributeError on CI.

Mirror the tuple to TestVoiceModePreferenceGate so the count assert resolves
to 10 with Italian present.

Co-authored-by: Samuel Gudi <samuel.gudi.official@gmail.com>
2026-05-11 23:14:20 +00:00
nesquena-hermes 0ee2a19cd8 Merge PR #2100 into stage-340 2026-05-11 23:12:59 +00:00
nesquena-hermes 4443e5845e docs: CHANGELOG v0.51.46 Release V (5-PR batch + 3 Opus SHOULD-FIX) 2026-05-11 17:53:38 +00:00
nesquena-hermesandOpus advisor 96ca83bf53 fix(security): drop unsafe-eval + add jsdelivr to CSP, sanitize plugin error
Opus stage-339 review SHOULD-FIX items:

1. server.py: drop 'unsafe-eval' from CSP report-only policy.
   Verified by grepping all production JS — zero matches for eval(),
   new Function(), or string-form setTimeout/setInterval. Keeping it
   was a gratuitous privilege.

2. server.py: add https://cdn.jsdelivr.net to script-src + style-src.
   index.html loads Prism/xterm/katex from this CDN with SRI hashes —
   without the allowance every page load fires known-good CSP violations
   that drown out real signal once a collector is wired.

3. api/commands.py: sanitize plugin command error. Previously returned
   f'Plugin command error: {exc}' which would leak paths/env from
   FileNotFoundError('/etc/something/secret.key') etc. Now returns only
   the exception type name; full traceback goes to server log.

Test asserts updated to match the new policy shape.

Co-authored-by: Opus advisor <opus-advisor@hermes.local>
2026-05-11 17:53:02 +00:00
nesquena-hermes 55fbe66c79 docs: CHANGELOG Unreleased — stage-339 (5-PR batch + turn-journal stack) 2026-05-11 17:44:34 +00:00
nesquena-hermes fd069155af Merge PR #2062 into stage-339
feat: record turn journal lifecycle events
by @ai-ag2026
2026-05-11 17:43:58 +00:00
nesquena-hermes f6ce79185c Merge PR #2059 into stage-339
feat: add crash-safe turn journal writer
by @ai-ag2026
2026-05-11 17:43:58 +00:00
nesquena-hermes 2a1244f342 Merge PR #2089 into stage-339
support slash commands implemented in hermes plugin
by @plerohellec
2026-05-11 17:43:57 +00:00
nesquena-hermes 0456fb5619 Merge PR #2085 into stage-339
fix(logs): clipboard fallback + severity filter for Logs panel (#2081)
by @bergeouss
2026-05-11 17:43:56 +00:00
nesquena-hermes 9db1da76bd Merge PR #2084 into stage-339
fix: add report-only CSP header
by @ai-ag2026
2026-05-11 17:43:55 +00:00
nesquena-hermes 85ae0acbdc docs: CHANGELOG v0.51.45 Release U (9-PR batch + Opus SHOULD-FIX) 2026-05-11 17:31:18 +00:00
nesquena-hermesandOpus advisor 83de9d0cf0 fix(providers): log warning when custom provider entry yields empty slug
Opus stage-338 review SHOULD-FIX: silent drop at api/providers.py:1049
was diagnostically opaque. logger.warning() now surfaces the bad
config entry so operators can spot misconfigurations.

Co-authored-by: Opus advisor <opus-advisor@hermes.local>
2026-05-11 17:30:56 +00:00
nesquena-hermes 87bd9ea372 docs: CHANGELOG Unreleased — stage-338 (9 PRs) 2026-05-11 17:18:16 +00:00
nesquena-hermes 6a016dae6c Merge PR #2077 into stage-338
Refactor compression anchor visibility helpers
by @franksong2702
2026-05-11 17:17:25 +00:00
nesquena-hermes 98b6925333 Merge PR #2065 into stage-338
Fix session recovery polish
by @franksong2702

# Conflicts:
#	CHANGELOG.md
2026-05-11 17:17:24 +00:00
nesquena-hermes 0662f0986f Merge PR #2056 into stage-338
Fix custom provider name slugs with ports
by @franksong2702

# Conflicts:
#	CHANGELOG.md
2026-05-11 17:17:19 +00:00
nesquena-hermes 4388cb1a10 Merge PR #2068 into stage-338
fix(ui): prevent stuck sidebar spinner on completed sessions (closes #2066)
by @franksong2702
2026-05-11 17:17:05 +00:00
nesquena-hermes 2bfd538714 Merge PR #2063 into stage-338
fix: keep explicit forks out of lineage report
by @dso2ng
2026-05-11 17:17:05 +00:00
nesquena-hermes ee6c67f30c Merge PR #2074 into stage-338
Fix HERMES_HOME skill cache patching
by @franksong2702
2026-05-11 17:17:04 +00:00
nesquena-hermes da6b897e54 Merge PR #2076 into stage-338
test: add kanban locale parity check (refs #1973)
by @bergeouss
2026-05-11 17:17:03 +00:00
nesquena-hermes d87b23e76f Merge PR #2073 into stage-338
test: allow top-level markdown docs
by @ai-ag2026
2026-05-11 17:17:01 +00:00
nesquena-hermes 7037b084de Merge PR #2088 into stage-338
docs(themes): align THEMES.md with Theme × Skin architecture
by @michael-dg
2026-05-11 17:17:00 +00:00
nesquena-hermes b34643b92c docs(contributors): refresh contributor stats to v0.51.44
Update CONTRIBUTORS.md and the README contributors section to reflect
130 contributors and 568 PR credits as of v0.51.44 (was 66/142 at
v0.50.245). The numbers grew because:

- The previous refresh was 1 release-cycle ago (50+ tags + 8 batch
  releases of contributor PRs ago).
- The new counting rule explicitly includes closed-but-absorbed PRs:
  PRs whose original branch shows "closed" on GitHub but whose content
  shipped via batch-release squash with a Co-authored-by trailer, or
  via salvage rewrite with CHANGELOG attribution. This better reflects
  what users actually contributed.

The compilation pipeline:

1. Pull every closed PR from gh api (state=closed, both merged and
   unmerged on GitHub) — 1421 PRs.
2. Walk CHANGELOG.md release-by-release and extract:
   - `PR #N by @user` (canonical bullet form)
   - `(#N by @user`, `(PR #N by @user`, `(#N, @user;`
   - `PRs #A, #B by @user` (plural)
   - `@user — PR #N`, `@user — N PR (#A, #B)`
   - `(credit: @user)` and `(credit: @userA and @userB)`
3. For every PR# mentioned in CHANGELOG, union the explicit @-attributed
   users with the gh PR author (when external). Maintainer accounts
   (@nesquena, @nesquena-hermes) are excluded.
4. For PRs merged on GitHub but not mentioned in CHANGELOG (very early
   PRs, non-noteworthy direct merges), credit the gh author.
5. Three salvaged-design contributors not directly in CHANGELOG are
   credited in the special-thanks roll: @indigokarasu (#213 →
   v0.50.0 design language), @andrewy-wizard (#177 → initial Chinese
   locale absorbed into v0.42.0), @zenc-cp (#133 → anti-hallucination
   guard absorbed into streaming.py).

Pre-cleaning step strips HTML entities (`&#10;` etc.) before PR# scan
to avoid false matches. PR# regex requires a whitespace/paren/bracket
preceder so identifiers like `--key=123` and `(##10`-style headings
don't pollute the count.

Per-user first/last release computed from:
- For merged-on-GH PRs: the smallest tag whose creator-date is >= the
  PR's merged_at timestamp.
- For absorbed PRs: the release section in CHANGELOG that explicitly
  attributes to the user (or the earliest release that mentions the
  PR# if no explicit attribution exists for that user).

CONTRIBUTORS.md sections:
- Top contributors (5+ PRs) — 20 people, ranked
- Sustained contributors (3–4 PRs) — 11 people
- Two-PR contributors — 14 people, flat list
- Single-PR contributors — 85 people, flat list
- How credit is tracked — four paths described
- Special thanks — 11 highlight blurbs

README contributors section trimmed to top-10 table + notable-
contribution blurbs (29 distinct contributors mentioned with concrete
PR numbers). Same data, condensed for the README.

No code changes. Docs only.
2026-05-11 06:59:42 +00:00
nesquena-hermes cd7107cefb test(infra): identity check by qname (CI re-imports conftest under multiple roots)
CI's pytest invocation imports conftest twice (once via the standard
tests/ discovery, once via repo-root rootdir discovery), producing two
distinct function objects with the same __qualname__ but different `is`
identity. The strict identity assertion failed because each import
created a fresh closure. Switch to __qualname__ substring check — same
guarantee (default-on state has the wrapper installed; fixture restores
the real one) without the multi-import sensitivity.
2026-05-11 06:18:13 +00:00
nesquena-hermes d9bc8360a4 test(infra): fixture swaps real functions via monkeypatch (CI-robust)
CI on Python 3.11 still failed test_allow_outbound_network_fixture_*
because the previous module-global toggle (_ALLOW_OUTBOUND=True/False)
was unreliable on the runner — the wrapper's global lookup at call time
sometimes saw False even after the fixture's True assignment.

Switch to monkeypatch-based fixture: instead of toggling a global that
the wrapper checks, restore socket.create_connection and
socket.socket.connect to their REAL captured implementations for the
duration of the test. Pytest's monkeypatch fixture handles teardown so
the wrappers are reinstalled automatically.

Rewrote the two paired tests to check function identity
(socket.create_connection is _hermes_blocked_create_connection vs. is
_REAL_CREATE_CONNECTION) instead of attempting a live outbound to
8.8.8.8:53 — direct identity check is hermetic and doesn't depend on
whether the CI runner has any outbound network access at all.
2026-05-11 06:15:46 +00:00
nesquena-hermes 6d83d16016 test(infra): tighten IPv6 unique-local check + replace self-passing fixture test
Two low-severity follow-ups from Opus regrounding review:

1. The IPv6 unique-local fc00::/7 check was `h.startswith('fc') or
   h.startswith('fd')` — too loose. It would also classify hostnames
   like 'food.example.com' or 'fdsa.test' as 'local' and silently let
   them through the block. Tightened to a regex match for canonical
   IPv6 syntax (`f[cd][0-9a-f]{0,2}:`) so only actual IPv6 addresses
   match. Same fix in both tests/conftest.py and server.py.

2. test_allow_outbound_network_fixture_unblocks was technically
   self-passing: it tried to connect to a *.invalid hostname, which is
   in the allow-list, so the real socket.create_connection would run
   regardless of whether the fixture toggled the block. Replaced with
   a public-IP-based test that actually proves the toggle works, plus
   a paired test_block_is_active_outside_the_fixture sanity test that
   proves the block is on without the fixture.

Both follow-ups noted by Opus advisor as 'defer-OK' but trivial fixes
so landing them in this batch.
2026-05-11 06:12:07 +00:00
nesquena-hermes 23cfc99738 fix(config): split hermes_cli and urlopen fallback in lmstudio branch (CI fix)
CI on Python 3.13 (clean editable install, no hermes_cli package) was still
failing the 3 lmstudio tests after the first fix attempt. Root cause: the
outer try/except in the lmstudio branch was catching ImportError from
`from hermes_cli.models import provider_model_ids`, hijacking the whole
branch and silently skipping the urlopen fallback.

Restructured into two independent tiers:
  1. hermes_cli lookup in its own try/except — ImportError logs at DEBUG
     and continues with lm_ids=[].
  2. urlopen fallback runs unconditionally when lm_ids is empty, including
     after hermes_cli import failure.

New regression test `test_lmstudio_fallback_works_when_hermes_cli_unavailable`
explicitly blocks hermes_cli via sys.meta_path and verifies the lmstudio
group still populates from the urlopen fallback. Without this test, the
CI-vs-local divergence (local env had hermes_cli installed, CI didn't)
would keep slipping through.

All 12 lmstudio-related tests pass, including the 3 #1527 tests that
broke on stage-337.
2026-05-11 06:06:58 +00:00
nesquena-hermes 1819ead93d docs: CHANGELOG v0.51.44 Release T (5-PR batch + test network isolation) 2026-05-11 06:03:12 +00:00
nesquena-hermes 12cef733e3 fix(recovery): preserve worktree metadata + workspace + message_count on state.db sidecar rebuild
PR #2053 added worktree-backed session creation. PR #2041 (shipped in
v0.51.42) added state.db sidecar reconciliation that rebuilds a missing
<sid>.json sidecar from the canonical state.db row when the JSON file is
gone (failed save, manual rm, restore-from-backup with mismatched dirs).

The two interact silently. `_state_db_row_to_sidecar()` was hard-coding
`'workspace': ''` and never propagating the four worktree_* fields from
the row to the rebuilt sidecar dict. So a worktree-backed session that
loses its sidecar and gets rebuilt from state.db:

- loses `worktree_path` → matches the empty-session sidebar filter at
  `api/models.py:1067/1107` (which spares worktree-backed empty sessions
  via `not s.get('worktree_path')`) → session disappears from the
  sidebar even though the worktree directory still exists on disk.

- loses `workspace` → downstream tools (terminal panels, file pickers
  that use `s.workspace`) operate on empty string instead of the original
  worktree path.

- always reports `message_count == 0` → contributes to the empty-session
  filter even for sessions that have messages in `state.db.messages`.

Fix:

1. `_read_state_db_missing_sidecar_rows()` SELECT now includes
   `workspace, worktree_path, worktree_branch, worktree_repo_root,
   worktree_created_at, message_count` (each gated by
   `_sql_optional_col()` so older state.db schemas without those columns
   continue to work — recovery degrades gracefully rather than 500ing).

2. `_state_db_row_to_sidecar()` propagates each field. workspace comes
   from the row if it's a string, otherwise '' (matching pre-fix behavior
   for non-worktree sessions). message_count comes from the row if
   it's an int, otherwise falls back to `len(messages)` so the rebuilt
   sidecar always has a coherent count.

3 new regression tests in tests/test_state_db_worktree_recovery.py
exercise:
- worktree session with messages → all four worktree_* fields preserved.
- non-worktree session → worktree_* fields all None (no spurious
  propagation), workspace=''.
- empty worktree session (the worst case) → confirms the rebuilt sidecar
  does NOT match the empty-session-exempt filter, so it stays visible
  in the sidebar.

Caught by Opus advisor during stage-337 review (the cross-PR interaction
between #2053 and the previously-shipped #2041 wasn't exercised by either
PR's individual test suite).
2026-05-11 06:00:13 +00:00
nesquena-hermes 2ca220eec0 fix(config): PR #1970 lmstudio branch must honor cfg.model.base_url fallback
PR #1970 added a dedicated `elif pid == "lmstudio":` branch in
`get_available_models()` that fetches the live /v1/models list when the
hermes_cli helper doesn't have ids cached. The fallback path inside that
branch only looked at `cfg["providers"]["lmstudio"]["base_url"]`, missing
the historical config shape where the URL lives under `cfg["model"]`:

  model:
    provider: lmstudio
    base_url: http://192.168.1.22:1234/v1   ← here, not under providers.lmstudio
  providers:
    lmstudio:
      api_key: local-key

3 pre-existing tests in tests/test_issue1527_lmstudio_base_url_classification
broke on stage-337 because of this — they passed on master, failed after
the PR #1970 merge.

The simpler fix is to enhance the already-introduced `_get_provider_base_url()`
helper so it falls back to `cfg["model"]["base_url"]` when
`cfg["model"]["provider"] == provider_id`, then use the helper inside the
lmstudio branch instead of a direct lookup. This keeps the previous
behaviour (where the generic configured-provider branch handled lmstudio
via the model block) while preserving PR #1970's live-discovery additions.

Belt-and-suspenders: `_get_provider_base_url()` explicitly does NOT inherit
model.base_url for providers other than the active one — if a user's config
says `model.provider: anthropic` and they have `providers.openai` configured
without a base_url, openai must still resolve to None (use SDK default),
not to the anthropic proxy URL.

6 new regression tests in tests/test_pr1970_lmstudio_base_url_fallback.py
lock the two-location lookup, the precedence rule (explicit providers entry
wins over model fallback), trailing-slash stripping, and the negative case
(model.base_url MUST NOT leak to non-active providers).

All 51 tests in the existing model-resolver + custom-provider banks still
pass.

Caught by maintainer review on stage-337 (full pytest with the new network
isolation in place surfaced the regression that the fork-CI mock-server path
would have hidden).
2026-05-11 05:59:59 +00:00
nesquena-hermes a6174d08db test(infra): hermetic network isolation — block all outbound from tests
Tests should not reach the public internet. Before this commit, an
accidentally-leaking outbound socket from the test_server fixture (real
TLS handshakes to Anthropic / Amazon / OpenRouter, sometimes triggered
by SDK-init paths that found a credential the credential-strip allowlist
missed) was adding 60+s of wall-time to a 100s test run and creating a
class of flaky failures.

This installs a default-deny socket-block at two layers:

1. Pytest process, via tests/conftest.py module-level monkey-patch on
   socket.create_connection + socket.socket.connect. Loopback / RFC1918
   private / link-local / RFC2606 reserved-TLD destinations pass through;
   anything else raises OSError("hermes test network isolation: outbound
   to ... blocked"). Tests that legitimately need real outbound opt back
   in via the new `allow_outbound_network` fixture (no current callers).

2. Test_server subprocess (server.py), via a HERMES_WEBUI_TEST_NETWORK_BLOCK=1
   environment-variable-gated guard at the top of server.py. tests/conftest.py
   sets the env var on every test_server spawn. Without this, the subprocess
   could make outbound that the pytest-side block can't see (which is exactly
   what was happening — verified via `ss -tnp` showing the server.py child
   with established ESTAB sockets to [2607:6bc0::10]:443).

In production the env var is unset, so the guard is a no-op.

Companion changes:

- test_dns_resolution_failure refactored to mock socket.getaddrinfo
  raising gaierror, instead of relying on a real DNS lookup of a
  *.invalid hostname. The test was the one outlier that genuinely
  exercised real DNS; mocking matches what every other probe-error test
  in the same file already does.

- New tests/test_conftest_network_isolation.py with 9 adversarial
  tests proving the block fires for public IPs (including the exact
  Anthropic IPv6 and Amazon IPv4 destinations we observed leaking),
  the allow-list passes loopback / RFC1918 / link-local / reserved-TLDs,
  and the opt-in fixture re-enables real outbound when needed.

Test suite: 5,120 → 5,192 (+72 net new from this commit + the regression
tests in the companion commits). Wall time: 161s → 95s on the same
hardware. No remaining outbound from any test path.
2026-05-11 05:59:42 +00:00
nesquena-hermes d86dcc12c6 Merge PR #2055: fix: duplicate assistant transcript merge 2026-05-11 05:12:05 +00:00
nesquena-hermes e0ecf2a035 Merge PR #1970: feat: LM Studio provider with live model discovery 2026-05-11 05:12:04 +00:00