Commit Graph

745 Commits

Author SHA1 Message Date
Lucas Coutinho bc3f4e54a6 Cache PBKDF2 password hash to eliminate ~1s overhead on every HTTP request
get_password_hash() computes PBKDF2-SHA256 with 600k iterations to
hash the HERMES_WEBUI_PASSWORD env var.  This is called on nearly every
HTTP request via check_auth -> is_auth_enabled -> get_password_hash.

Before: ~1s of PBKDF2 per request, regardless of how many times the
same env-var value has already been hashed.  A page load hitting 5+
API endpoints would burn 5+ seconds purely on password hashing.

After: compute once on first call, cache the hex result in a module-
level variable.  Subsequent calls are a single global-variable read
(~50ns).  The env var is immutable for the process lifetime, so there
is nothing to invalidate.

Thread-safe: double-checked locking ensures that under a burst of
concurrent requests only one thread computes PBKDF2, while the fast
path (after initialisation) requires zero locks.

Security analysis: zero regression.  The hash is derived from a static
env var and a static signing key — both already readable from process
memory.  Caching does not introduce any new disclosure or replay
vector.  PBKDF2 is still used for the initial computation and for
verify_password() on login.

AI: deepseek/deepseek-v4-flash
2026-05-13 00:25:41 -03:00
Hermes Agent 20717a0d0a Merge pull request #2136 into stage-345
fix: guard stale stream writebacks (LumenYoung)

Prevents stale WebUI stream workers from writing old results into a session
after that session has already moved on to another stream. Adds new helper
_stream_writeback_is_current() (a token equality check against the session's
active_stream_id) and short-circuits the two finalize/cancel paths when the
worker no longer owns the session writeback.
2026-05-12 23:11:48 +00:00
Lumen Yang 4b57b202a0 fix: guard stale stream writebacks 2026-05-13 00:05:09 +02:00
Hermes Agent 2def05f385 stage-344: apply Opus SHOULD-FIX #1+#2 — #2128 multi-tab race + stale-done re-emit
(1) compress/status no longer pops the job entry on first read of `done` payload.
    Second open tab no longer sees `idle` and a stale-job toast.
(2) compress/start no longer short-circuits to a stale `done` payload when
    re-invoked within the 10-minute TTL. Re-running /compress always starts
    fresh, so closing-and-reopening a tab mid-compress works correctly.

Third SHOULD-FIX (#2135 cfg["model"] fallback tightening when no custom_providers
entry matches) deferred to follow-up — strictly no-worse-than-master behavior.

tests/test_sprint46.py 10/10 still passes.
2026-05-12 16:37:37 +00:00
Hermes Agent 7116c680df stage-344: maintainer fix for #2142 fr locale — add LOCALES tuple entries + _LOGIN_LOCALE block
#2142 (legeantbleu) added the fr locale to static/i18n.js but didn't update:
1. tests/test_issue1488_composer_voice_buttons.py: two TestComposerVoiceButtonI18n + TestVoiceModePreferenceGate LOCALES tuples needed 'fr'
2. api/routes.py: _LOGIN_LOCALE needed an 'fr' block so the login page localizes for French users (issue #1442 parity contract)
3. tests/test_login_locale_parity.py: the test asserting 'fr' falls-back-to-'en' is inverted — fr now resolves to fr, with sibling assertions for fr-FR and fr-CA

Mirrors the stage-340 fix for the it locale (PR #2067 → maintainer adds tuple entries). 46/46 i18n tests pass after fix.
2026-05-12 16:14:47 +00:00
Hermes Agent c677c19a8f Merge pull request #2128 into stage-344
Fix manual compression proxy timeouts (closes #2087)

# Conflicts:
#	CHANGELOG.md
2026-05-12 16:13:01 +00:00
Hermes Agent 1ee8627acb Merge pull request #2135 into stage-344
Fix custom live model scoping (closes #2126, refs #2131)

# Conflicts:
#	CHANGELOG.md
2026-05-12 16:13:00 +00:00
Hermes Agent aa85bd2e7c Merge pull request #2138 into stage-344
fix: recover from stale deleted workspaces
2026-05-12 16:12:58 +00:00
Hermes Agent 8dd0b4ec31 Merge pull request #2139 into stage-344
fix: audit turn journal terminal collisions
2026-05-12 16:12:56 +00:00
Hermes Agent a06952ab00 Merge pull request #2140 into stage-344
Preserve fallback provider credential hints (closes #2133)

# Conflicts:
#	CHANGELOG.md
2026-05-12 16:12:54 +00:00
Frank Song 76e611d49f Preserve fallback provider credential hints 2026-05-12 20:42:55 +08:00
dobby-d-elf 516d942d6a refactor: reduce stale workspace recovery fix 2026-05-12 06:28:35 -06:00
Michael Lam f5f59a5813 fix: audit turn journal terminal collisions 2026-05-12 05:20:06 -07:00
Frank Song b7c5ba640c Fix custom live model scoping 2026-05-12 20:05:28 +08:00
dobby-d-elf e03c197cdf fix: recover from stale deleted workspaces 2026-05-12 05:52:16 -06:00
Frank Song 8fa92c680f Fix manual compression proxy timeouts 2026-05-12 17:33:59 +08:00
Michael Lam 265496782a docs: clarify compression anchor helpers 2026-05-12 01:43:16 -07:00
Hermes Agent 10cfcee30e stage-342: apply Opus SHOULD-FIX — tighten worktree status _run_git timeout 5s → 2s
Worst case 4×5s=20s per polling request on ThreadingHTTPServer pool is risky
given today's _cron_env_lock near-miss on production 8787. Status probes
should fail fast; client can retry. All four call sites use default timeout.
2026-05-12 05:22:01 +00:00
Hermes Agent 4d64f6eee9 Merge pull request #2116 from starship-s/fix/codex-quota-pool-usage
fix(providers): load Codex quota from credential pool
2026-05-12 05:10:23 +00:00
starship-s 573fc25f96 fix(providers): load Codex quota from credential pool 2026-05-11 21:46:24 -06:00
Frank Song 6e1e9fafbe Add worktree status endpoint 2026-05-12 10:08:01 +08: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
Frank Song 2da4f108c5 Clarify worktree session archive/delete semantics
(cherry picked from commit f5c8fb58d1)
2026-05-12 00:05:05 +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 efeae4a86e)
2026-05-12 00:04:11 +00:00
Samuel Gudi ba3cc2c541 feat(i18n): add Italian (it) locale
Adds complete Italian translation for all ~280 UI strings in static/i18n.js
and the login page strings in api/routes.py (_LOGIN_LOCALE).

Ordered alphabetically: en → it → ja in both files.
Preserves all JS function templates, template literals, and plural forms.

(cherry picked from commit c66e04b190)
2026-05-11 23:13:55 +00:00
Lumen Yang e37c69cf57 fix(agent-health): treat stale running gateway as unknown
(cherry picked from commit 4be346fece)
2026-05-11 23:13:09 +00:00
ai-ag2026 52fedbc783 feat: add per-cron toast notification toggle 2026-05-11 21:58:35 +02:00
nesquena-hermes 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 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 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 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 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
Philippe Le Rohellec 281a57b60a support slash commands implemented in hermes plugin 2026-05-11 09:42:40 -07:00
ai-ag2026 c864ad47af fix: address turn journal lifecycle review 2026-05-11 17:16:43 +02:00
ai-ag2026 d04d48f5a0 fix: harden turn journal submitted writes 2026-05-11 17:13:57 +02:00
Frank Song 18124ced62 Refactor compression anchor visibility helpers 2026-05-11 20:56:30 +08:00
Frank Song a0e9c06102 Fix HERMES_HOME skill cache patching 2026-05-11 19:12:02 +08:00
Frank Song f6115b78c6 Fix custom provider name slugs with ports 2026-05-11 17:24:53 +08:00
Dennis Soong 5efd287264 fix: align fork lineage projection paths 2026-05-11 17:15:22 +08:00
Frank Song 2cd10868aa Fix session recovery polish 2026-05-11 16:30:25 +08:00
Dennis Soong 1e8d65ea01 fix: keep explicit forks out of lineage report 2026-05-11 15:23:52 +08:00
ai-ag2026 4b486f2860 feat: record turn journal lifecycle events 2026-05-11 09:13:25 +02:00
ai-ag2026 5cd001d545 feat: add crash-safe turn journal writer 2026-05-11 08:49:53 +02: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 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