Commit graph

3567 commits

Author SHA1 Message Date
nesquena-hermes
7e9f674edf fix(#3263): provider-compatible default match — reject same-bare-name on different providers (Codex over-match MUST-FIX)
Prior round stripped provider prefixes from both sides and matched bare-only,
which over-matched: openai/gpt-4o would match default openrouter/gpt-4o. Now the
matcher compares bare model ids AND rejects when both sides identify DIFFERENT
providers (from provider/ prefix, @provider: qualifier, or the explicit provider
arg). Same-provider / unknown-session-provider still match. Added cross-provider
rejection regression tests.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 01:19:49 +00:00
nesquena-hermes
4147e7f3df fix(#3263): provider-aware default-model match (Codex final-gate MUST-FIX)
The default-only context_length guard compared model.default to the session
model with exact string equality. But model.default and the session model can
be stored in equivalent-but-different shapes (bare 'claude-opus-4.8',
provider-prefixed 'anthropic/claude-opus-4.8', or '@anthropic:claude-opus-4.8').
An exact compare wrongly treats the actual default model as non-default and
drops its configured context_length cap for provider-prefixed configs.

Add api/routes._model_matches_configured_default(session_model, cfg_default,
provider) that normalizes all three shapes, and use it at all 6 guard sites
(routes resolver + the 5 api/streaming.py sites: live-usage snapshot, persistence
_skip_cc_cl, persistence fallback _apply_cfg_ctx, SSE-done _dropped_stale_cap_sse,
SSE fallback _apply_cfg_ctx). Imported function-scoped in streaming to avoid the
routes<->streaming module-level circular import. 10 helper unit tests + a
behavioral test that a prefixed default still receives its cap.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 01:14:22 +00:00
nesquena-hermes
17e7794923 fix(#3263): init _skip_cc_cl/_cc_cl before compressor block (UnboundLocalError on no-compressor path) + relax brittle #1318 source-assertion
The threshold-rescale block runs unconditionally after the fallback and
references _skip_cc_cl/_cc_cl, which were only defined inside 'if _cc_for_save:'.
On the no-compressor path (fresh agent / interrupted stream) that raised
UnboundLocalError (caught by test_issue1857_usage_overwrite). Hoist both inits
above the block (no-op rescale when no compressor). Also widen the #1318
source-assertion test to accept the widened fallback gate (still asserts the
falsy-check invariant).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 00:14:13 +00:00
nesquena-hermes
18a459726b fix(#3263): apply Codex dual-gate MUST-FIX — recompute persisted stale cap + rescale terminal threshold
Codex regression gate (+Opus, both independently) found the default-only guard
dropped the stale compressor cap but two sibling paths stayed inconsistent:
1. Per-turn persistence: fallback resolver only ran when context_length was
   falsy, so a previously-persisted stale 232K survived forever on non-default
   sessions. Now also runs when _skip_cc_cl, and rescales threshold_tokens to
   the recomputed real cap (or clears it).
2. Terminal SSE 'done' payload: re-emitted the stale compressor threshold, so
   messages.js overwrote S.lastUsage and the indicator reverted on stream end.
   Now rescales threshold to the resolved window when the stale cap was dropped.
Added 3 source-structure regression tests pinning both fixes; bumped the brittle
test_pr1341 distance limit 13000→15000 (+ noted it should become structural).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 00:09:21 +00:00
nesquena-hermes
7a734462b0 docs(changelog): v0.51.192 Release FL (stage-batch4) 2026-05-31 23:52:28 +00:00
nesquena-hermes
5768a4bfc8 test(#3263): bump brittle save-distance limit + add default-only context_length guard regression test
- test_pr1341 distance limit 9000→13000 (the PR legitimately added the
  default-only pre-save guard block; the test is designed to be bumped when a
  new pre-save mutation block lands — this was the only CI-red cause on shard 1).
- new tests/test_issue3256_context_length_default_only_guard.py: verifies the
  global model.context_length cap applies ONLY to model.default (revert-fix-
  verified — fails on master, passes with the fix).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-05-31 23:51:49 +00:00
allenliang2022
cba69cd415 fix(context): rescale threshold_tokens to real cap in live usage snapshot
The default-only guard corrected context_length to the real per-model cap
(e.g. 1M for claude-opus-4.7-1m) but left threshold_tokens pointing at the
ContextCompressor's stale value (computed from the global 232K cap → 197.2k
@ 85%). UI then showed 'auto-compress at 197.2k / 1M' which is misleading.

Rescale threshold_tokens by the real/orig ratio so the displayed trigger
reflects the actual window (e.g. ~850k @ 1M).

NOTE: this only corrects the SSE display payload. The real auto-compress
trigger lives inside ContextCompressor in hermes-agent (agent_init.py:1446
constructs it with the global cap). A full fix requires a parallel change
upstream — tracked separately.
2026-05-31 23:49:41 +00:00
allenliang2022
1263cf03cf perf(context): cache real per-model context_length in live usage snapshot
_live_usage_snapshot() runs on every metering tick (~10x/sec while streaming).
The #3256 default-only guard recomputed get_model_context_length() there on
every tick for non-default models, which does a config read + potential
metadata/network probe — freezing claude-opus-4.7-1m streams while the default
model (4.8) stayed fast (guard not triggered for it). Resolve the real cap at
most once per stream via _real_ctx_cache. Backend-only, no frontend changes.
2026-05-31 23:49:41 +00:00
allenliang2022
4785aa542a fix(context): re-apply default-only context_length guard after v0.51.185 upgrade
Re-applies cb0065eb + b34311b3 (context_length default-only guard) which were
dropped by the upgrade reset to v0.51.185. Fixes 4.7-1m context window showing
as stale global cap (232K) instead of real 1M metadata. Backend-only: touches
api/routes.py + api/streaming.py, zero frontend/render changes.
2026-05-31 23:49:40 +00:00
nesquena-hermes
0488adc695
Merge pull request #3292 from nesquena/release/stage-batch3
v0.51.191: skills-detail markdown styling (#3284) + launchd duplicate-start guard (#3291) (stage-batch3)
2026-05-31 16:20:44 -07:00
nesquena-hermes
e86488072a fix(#3291): make launchd duplicate-start guard port-aware (Codex gate MUST-FIX)
Codex regression gate found the launchd guard blocked ANY ctl.sh start while a
launchd job was live — including a legitimate second instance on a different
port (HERMES_WEBUI_PORT=8788). Now _launchd_webui_pid only treats the launchd
job as a conflict when its PID is actually listening on the requested CTL_PORT
(via a new best-effort _pid_listens_on_port helper using lsof); a different-port
start is allowed. When port ownership can't be determined (no lsof), falls back
to guarding only the default 8787 port so non-default ports are never wrongly
blocked. Adds a different-port-allowed regression test + makes the existing
block test deterministic. Co-authored-by preserved.
2026-05-31 23:08:52 +00:00
nesquena-hermes
15fddb84b6 docs(changelog): v0.51.191 Release FK (stage-batch3) 2026-05-31 23:02:52 +00:00
Andy Kang
91717b7e47 fix: block duplicate webui start when launchd owns 8787 2026-05-31 23:02:23 +00:00
Pamnard
f24d633189 Fix skills detail markdown styling with preview-md wrapper
Skill detail and linked markdown files now use the same preview-md
pipeline as Memory/Notes, with code highlighting and KaTeX enhancement.
2026-05-31 23:02:23 +00:00
nesquena-hermes
e16f699313
Merge pull request #3287 from nesquena/add-github-funding
chore: add GitHub Sponsors funding config
2026-05-31 15:40:19 -07:00
nesquena-hermes
717f4688d3 chore: add GitHub Sponsors funding config
Adds .github/FUNDING.yml so GitHub displays a "Sponsor" button on the
repository, linking to GitHub Sponsors for @nesquena.
2026-05-31 22:26:35 +00:00
nesquena-hermes
e459acd81a
Merge pull request #3282 from nesquena/release/stage-batch2
v0.51.190: Windows upgrade state-stranding hotfix (#2905) + gateway banner (#3194) + quiet tool previews (stage-batch2)
2026-05-31 12:44:46 -07:00
nesquena-hermes
1aed605fb6 fix(#3267): harden collapsed tool-preview secret filter (Codex gate MUST-FIX)
Codex regression gate found the exact-name hidden-key set leaked secret-shaped
args (apiKey/access_token/clientSecret/Authorization/cookie/...) into the
always-visible collapsed tool-card header. Replace with a normalized
case-insensitive _toolArgPreviewKeyIsHidden() predicate matching secret-bearing
substrings + camelCase variants. Adds 22 parametrized regression tests pinning
the secret-key denial + a legit-key-still-shown guard. Co-authored-by preserved.
2026-05-31 19:21:15 +00:00
nesquena-hermes
639a88e937 docs(changelog): v0.51.190 Release FJ (stage-batch2) 2026-05-31 19:12:56 +00:00
ai-ag2026
69072ac34d fix: keep collapsed tool previews quiet 2026-05-31 19:12:31 +00:00
nesquena-hermes
5742a23836 fix: apply Opus+Codex gate findings — stale-stopped not configured (#1944), narrow #2905 markers, scope except
Both pre-release reviewers (Opus advisor + Codex regression gate) converged on
the same MUST-FIX:

- #3194: treating gateway_stale_stopped_state as 'configured' contradicted
  #1944 (a stopped root gateway should read like 'not configured' so the
  banner doesn't nag). Now ONLY stale-RUNNING metadata
  (reason=gateway_stale_running_state or gateway_state=='running') flips
  configured=True; stale-stopped falls through to bool(identity_map) like the
  genuinely-unconfigured case. Updated the test accordingly + added a
  stale-stopped no-regression test.

Opus follow-ups also applied:
- #2905: narrowed the populated-home markers to WebUI-only artifacts
  (webui/, webui/sessions, webui/settings.json), dropping config.yaml/auth.json
  so a long-time agent user installing WebUI fresh isn't wrongly diverted to
  the legacy %USERPROFILE%\.hermes (auth.json predates #2897 there).
- profiles._resolve_base_hermes_home(): narrowed except Exception -> ImportError
  so a real bug in the config helper still surfaces.

Adjacent suites green: #2840, #1879, gateway_status_agent_health (66 tests).
2026-05-31 19:12:30 +00:00
nesquena-hermes
5dc11ef977 fix: Windows upgrade state stranding (#2905) + gateway-configured banner on first deploy (#3194)
Two urgent breaking bugs that had no PR, combined into one hotfix.

#2905 (data-loss-class): v0.51.134 moved the Windows default Hermes home
from %USERPROFILE%\.hermes to %LOCALAPPDATA%\hermes (PR #2897) with no
migration, so upgrading Windows users opened the app to empty
sessions/pins/settings (data intact on disk, at an address the new build
no longer read). _platform_default_hermes_home() now prefers the populated
legacy home ONLY when the new location is not yet established —
non-destructive (no file moves) and self-healing on next launch.
profiles._resolve_base_hermes_home() delegates to the same config helper so
the active-profile pointer can never drift from STATE_DIR.

#3194: GET /api/gateway/status reported 'Gateway not configured' on a fresh
two-container Docker deploy because an alive=None + gateway_stale_running_state
health payload with an empty identity_map fell through to
configured=bool(identity_map)=False. The alive=None branch now treats a
payload carrying gateway metadata (gateway_state detail, or a stale-running/
stale-stopped reason) as configured.

Tests: +17 regression tests (11 for #2905 incl. full truth table + non-destructive
guard + POSIX no-op; 6 for #3194 incl. 5 no-regression guards). Full suite
7090 passed, 0 failed.

Closes #2905
Closes #3194
2026-05-31 19:12:30 +00:00
nesquena-hermes
1d4d40b16d
Merge pull request #3277 from nesquena/release/stage-batch1
v0.51.189: ruff lint gate + SSE refresh dedupe + tooltip i18n (stage-batch1)
2026-05-31 11:39:37 -07:00
nesquena-hermes
a92c23c4fa docs(changelog): v0.51.189 Release FI (stage-batch1) 2026-05-31 18:27:00 +00:00
nesquena-hermes
e58fb6a201 ci: forward-looking ruff lint gate (E9+F+B, new-code-only) (#3275)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-05-31 18:26:37 +00:00
ai-ag2026
e34c632236 fix: localize WebUI tooltip quick wins 2026-05-31 18:26:31 +00:00
PINKIIILQWQ
aeda6add2b fix: suppress phantom sidebar refresh on gateway SSE reconnect
Shift from backend mtime-based detection to frontend SSE deduplication.

Backend: Revert gateway_watcher.py to original pure hash-based polling.
Remove _get_db_mtime, _detect_gateway_restart, and mtime tracking.
This is a no-op in behavior — the original was already hash-only.

Frontend: Add deduplication at the SSE event handler level.
- _gatewaySessionSnapshotKey(sessions): deterministic key from
  session_id + updated_at + message_count (same fields as backend hash)
- _isGatewaySessionForSnapshot(session): classify non-webui sessions
- _isDuplicateGatewaySessionSnapshot(sessions): compare SSE payload
  against current _allSessions, filtered to gateway subset
- SSE sessions_changed handler wraps renderSessionList() in dedupe:
  identical data → skip refresh

This directly addresses the real root cause: the SSE reconnect snapshot
(routes.py:7735) unconditionally pushes an initial snapshot, and the
frontend always re-renders. After this fix, a reconnect with unchanged
session data is correctly detected and the redundant redraw is skipped.

Previously submitted as #3259 (backend mtime approach, now closed per
maintainer review).
2026-05-31 18:26:30 +00:00
nesquena-hermes
78994c6a49
Merge pull request #3274 from nesquena/release/stage-batchH
v0.51.188: configured runner-client boundary, default-off (batchH, #3073)
2026-05-31 10:55:47 -07:00
nesquena-hermes
ba7ae5786d harden: runner client rejects non-http(s) base_url + no redirect-follow (#3073)
Defense-in-depth flagged by both pre-release reviewers (Opus + Codex), both
non-blocking but cheap on a credential-handling surface:
- reject any non-http(s) HERMES_WEBUI_RUNNER_BASE_URL scheme at construction
  (a misconfigured file:// / ftp:// can never reach urlopen);
- route requests through an opener that does NOT follow 3xx redirects, so a
  misbehaving/compromised runner cannot smuggle the Bearer token to another host.
Both operator-misconfiguration-only (not user-reachable). +2 regression tests.

Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
2026-05-31 17:49:26 +00:00
nesquena-hermes
fd01d80ca6 docs(changelog): v0.51.188 Release FH (stage-batchH) 2026-05-31 17:37:39 +00:00
nesquena-hermes
33fbd9fc6d feat: add configured runner client boundary (#3073)
Opt-in HTTP runner-client boundary for the runner-local runtime adapter
(RFC hermes-run-adapter-contract / #1925, Slice 4c/4d). Default-OFF:
without HERMES_WEBUI_RUNNER_BASE_URL the factory preserves the bounded
'not configured' path; when set, WebUI acts only as a JSON HTTP client +
SSE bridge for start/observe/status/controls without owning runner maps.
New api/runner_client.py + additive _runner_* helpers in api/routes.py;
no change to the live _run_agent_streaming legacy path.

Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
2026-05-31 17:37:11 +00:00
nesquena-hermes
556f5d1c15
Merge pull request #3271 from nesquena/release/stage-batchG
v0.51.187: workspace-preview persistence + scroll-intent window (batchG)
2026-05-31 10:10:46 -07:00
nesquena-hermes
d46d3a1411 fix: canonicalize ./ and ~/ prefixes in _normalizeArtifactPath (#3262)
Pre-release Codex regression gate caught that _normalizeArtifactPath()
did not strip ./ or ~/ prefixes, so a tool arg recorded as ./foo.md did
not match a file-tree-opened foo.md in _turnMutatedPreviewPaths — the
open preview was left stale after an agent edit via a ./-prefixed path.
Strip ~/ and leading ./ before ignore/membership checks. Node-driven
regression test pins foo.md == ./foo.md == ~/foo.md and confirms the
existing ignore-dir / URL / empty rejections still hold.

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
2026-05-31 17:03:07 +00:00
nesquena-hermes
16bb1df44b test: pin #3250 scroll-intent window + fix #2823 openFile anchor for #3262
- Pin MESSAGE_UPWARD_INTENT_MS>=2000ms + intent-helper-uses-constant +
  downward-repin-independence (#3250, co-authored emanon312).
- Update the #2823 large-markdown-preview test's openFile() signature
  anchor for #3262's openFile(path, opts={}) extension.

Co-authored-by: emanon312 <emanon312@users.noreply.github.com>
2026-05-31 16:54:55 +00:00
Pamnard
ee414144d3 Reload open workspace preview when agent mutates that file
Track write/edit tool paths per turn, refresh the open preview on
tool_complete and after preservePreview loadDir on stream done, without
closing preview for unrelated responses or wiping unsaved local edits.
2026-05-31 16:54:22 +00:00
Pamnard
9365f2d219 Fix workspace preview closing on chat stream done
Background file-tree refresh after a response must not call clearPreview();
preserve the open preview while still reloading the directory listing.
2026-05-31 16:53:57 +00:00
emanon312
e24ca105d7 fix: extend upward scroll intent timeout to prevent streaming scroll snap-back
Increase MESSAGE_UPWARD_INTENT_MS from 450ms to 2000ms to fix a race
condition where the user scrolls up during streaming, pauses to read
for >450ms, and then gets snapped back to the bottom.

The root cause: after the 450ms upward-intent window expires, DOM layout
changes from the streaming markdown parser (smd), tool card insertions,
or code re-highlighting can trigger scroll events that the handler no
longer recognizes as user-initiated. When the resulting position lands
inside the 250px near-bottom zone for two consecutive samples, the
hysteresis counter re-pins (_scrollPinned=true) and the next streaming
token's scrollIfPinned() call forces scrollTop to the bottom.

With a 2-second window, the user's upward intent persists through typical
streaming DOM churn. Downward scrolling and the scroll-to-bottom button
are unaffected — movedUp requires top < _lastScrollTop-2 which is false
for downward movement regardless of the intent timeout.

Refs: #1360 (macOS momentum protection), #1731 (direction-aware unpin)
2026-05-31 16:53:57 +00:00
nesquena-hermes
e09bd013d9
Merge pull request #3269 from nesquena/release/stage-batchF
v0.51.186: update-checker ff-reachability fall-through + utf-8 git-output test coverage (batchF)
2026-05-31 09:33:52 -07:00
nesquena-hermes
02bdd214c1 docs(changelog): v0.51.186 Release FF (stage-batchF) 2026-05-31 16:18:31 +00:00
Pamnard
3a5cf6cda6 fix(updates): fall through when release tag is not ff-reachable
When agent checkouts track main past an older tag but the newest published
tag is on a divergent side branch, stop advertising tag-based updates and
route check/apply through the upstream branch instead.
2026-05-31 16:18:06 +00:00
zapabob
f17f582fc1 test: cover utf-8 git output decoding 2026-05-31 16:18:06 +00:00
nesquena-hermes
d4d87a901b
Merge pull request #3251 from nesquena/release/stage-batchE
Release v0.51.185 — Release FE (stage-batchE): clarify-card bug-fix batch
2026-05-30 23:58:51 -07:00
nesquena-hermes
907b00a449 docs(changelog): v0.51.185 Release FE (stage-batchE) 2026-05-31 06:50:40 +00:00
nesquena-hermes
997eff58ae Merge PR #3249 into stage-batchE 2026-05-31 06:50:11 +00:00
nesquena-hermes
86a1ddc3a7 Merge PR #3247 into stage-batchE 2026-05-31 06:50:10 +00:00
nesquena-hermes
584a3f0bf1 Merge PR #3245 into stage-batchE 2026-05-31 06:50:10 +00:00
allenliang2022
eddb17028d fix(updates): prevent startup crash on non-UTF-8 (GBK) locales
_run_git used subprocess.run(text=True) without an explicit encoding, so on
Chinese Windows (and other non-UTF-8 codepages) git stdout was decoded with the
locale codepage. _dirty_suffix() runs `git diff --binary HEAD`, whose binary
bytes are not valid GBK, raising UnicodeDecodeError in the subprocess reader
thread. That left r.stdout = None, so `r.stdout.strip()` raised AttributeError
during module import of api.updates, crashing server.py before it could bind
its port.

Force UTF-8 decoding with errors=replace and guard against None defensively.
2026-05-31 14:32:48 +08:00
mysoul12138
f9ff6df883 fix: prevent browser autofill on clarify input (#clarify-autofill)
Chrome's password manager aggressively autofills the clarify card's
input field with saved credentials (e.g. provider base URLs) despite
autocomplete='off'. This causes two bugs:

1. 'Clarification closed. Your draft was kept in the composer.' appears
   on every session completion because _stashClarifyDraft reads the
   autofilled value and treats it as a user draft.

2. The autofilled URL gets injected into the main composer, confusing
   the user.

Fix: add readonly attribute to the clarify input element so Chrome's
autofill ignores it. When showClarifyCard() makes the card visible,
readonly is removed programmatically so the user can type normally.

Both the static HTML (index.html) and the dynamic DOM creation
(_ensureClarifyCardDom in messages.js) are patched.
2026-05-31 13:54:02 +08:00
Andy Kang
18e9a6b9c9 fix: distinguish identical clarify prompts by id 2026-05-31 14:12:15 +09:00
nesquena-hermes
13c7f7157b
Merge pull request #3244 from nesquena/release/stage-batchD
Release stage-batchD → v0.51.184 (raw audio mode #3169 + scroll-preserve #3237 + non-POSIX test skip #3235)
2026-05-30 22:12:02 -07:00