Commit Graph

1698 Commits

Author SHA1 Message Date
nesquena-hermes f0ecd94e04 Stage 326: PR #1945 — Localize session jump controls by @franksong2702
# Conflicts:
#	CHANGELOG.md
2026-05-09 18:17:03 +00:00
nesquena-hermes 22ea145d49 Stage 326: PR #1950 — Mute stale stopped gateway heartbeat by @franksong2702 2026-05-09 18:16:16 +00:00
nesquena-hermes 979f30e46a Stage 326: PR #1960 — fix: translate hidden-files workspace label by @Michaelyklam 2026-05-09 18:16:16 +00:00
nesquena-hermes c2f0c6ccc0 Stage 326: PR #1961 — fix: WebUI respects image_input_mode — stop unconditionally embedding native images by @sbe27 2026-05-09 18:16:16 +00:00
nesquena-hermes 072ec41e0a Stage 326: PR #1947 — fix: show same model from different custom providers instead of deduplicating by @happy5318 2026-05-09 18:16:16 +00:00
nesquena-hermes 1c84da07fc Stage 326: PR #1953 — fix(config): skip #1776 provider peel for custom host:port slugs by @lucky-yonug 2026-05-09 18:16:16 +00:00
nesquena-hermes 9732795e9c Stage 326: PR #1957 — feat(auth): make session TTL configurable via env var and settings.json by @hermes-gimmethebeans 2026-05-09 18:16:16 +00:00
nesquena-hermes 7a0e4f1ee7 Stage 326: PR #1939 — test: cover theme-color media fallback by @ai-ag2026 2026-05-09 18:16:16 +00:00
nesquena-hermes 6f7479944c test(#1947): regression coverage for same-model-multiple-named-custom-providers
Adds tests/test_pr1947_same_model_multiple_custom_providers.py covering:

1. Two named custom providers exposing the same model id — both must
   surface in the rendered groups (one bare, one @custom:slug:model)
2. Three named providers all exposing the same model — none dropped
3. Distinct-model-per-provider sanity check (still grouped correctly)

Verified the regression-detecting tests (1 + 2) FAIL against master's
api/config.py (where _seen_custom_ids was seeded from auto_detected_models
and used as a global bare-id bucket — the second provider's entry was
silently dropped) and PASS against the contributor fix on this branch.

Test 3 (distinct-models sanity) passes either way as expected.

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: hacker1e7 <hacker1e7@users.noreply.github.com>
2026-05-09 18:15:50 +00:00
hermes-agent b443e8ea5a fix: WebUI respects image_input_mode — stop unconditionally embedding native images
_build_native_multimodal_message() unconditionally embedded images as
native image_url parts, bypassing the agent's image_input_mode config.

Add _resolve_image_input_mode(cfg) helper mirroring the agent's
decide_image_input_mode logic, and wire it into
_build_native_multimodal_message with a new cfg parameter.

When mode resolves to 'text' (explicit aux vision config, or
image_input_mode: text), returns plain string so the agent's
existing text-mode pipeline (vision_analyze) handles images.

Closes #1959
2026-05-09 19:39:50 +02:00
Michael Lam ce6685a27c fix: translate hidden-files workspace label 2026-05-09 10:36:30 -07:00
hermes-gimmethebeans 9d7c213971 feat(auth): make session TTL configurable via env var and settings.json
Add _resolve_session_ttl() with three-layer precedence:
  1. HERMES_WEBUI_SESSION_TTL env var (highest priority)
  2. session_ttl_seconds in settings.json
  3. Default: 86400 * 30 (30 days)

Clamped to [60s, 1 year] for safety. Settings changes take effect
immediately since the function is called dynamically at each login/cookie-write.

Closes #1954
2026-05-09 17:11:53 +00:00
happy5318 a6599cd68e fix: show same model from different custom providers instead of deduplicating
When multiple custom providers expose the same model ID (e.g. baidu,
huoshan, and liantong all offering glm-5.1), only the first provider's
entry was shown in the model dropdown.

Root cause (backend):  used the bare model ID as the
dedup key, so the second and subsequent providers with the same model
were silently skipped.

Root cause (frontend):  stripped the @provider: prefix before
comparing, so @custom:baidu:glm-5.1 and @custom:huoshan:glm-5.1 were
treated as duplicates.

Fix:
- Backend: change _seen_custom_ids key to '{slug}:{model_id}' so each
  provider's models are tracked independently.
- Frontend: add _providerOf() helper and deduplicate on the composite
  (normId, provider) key instead of normId alone. Bare model IDs
  (without @provider: prefix) still deduplicate on normId for backward
  compatibility.
2026-05-09 16:17:23 +08:00
liyang1116 7532482393 fix: fix(config): skip #1776 provider peel for custom host:port slugs
model_with_provider_context can emit @custom:<host>:<port>:<model> when
model_provider is derived from an OpenAI base_url authority (e.g.
custom:10.8.0.1:8080). The colon-count heuristic meant for @custom:slug:model:free
mistook those extra colons for an over-split model ID and prepended the port
segment onto the bare model (8080:Qwen3-235B), breaking WebUI while CLI/curl
stayed correct.

Detect endpoint-style slugs (IPv4/localhost/hostname + numeric port) and skip
the peel in that case. Add regression tests for IPv4, dotted hostname,
localhost, and model_with_provider_context round-trip.
2026-05-09 16:16:32 +08:00
Frank Song b38cc2f1ea Mute stale stopped gateway heartbeat 2026-05-09 14:53:42 +08:00
Frank Song 3dfd692d75 Localize session jump controls 2026-05-09 10:03:27 +08:00
ai-ag2026 5dcb4e9ade test: cover theme-color media fallback 2026-05-08 23:51:24 +02:00
nesquena-hermes 0b7e1e60e8 Release v0.51.30 — Release G (offline recovery + PWA hardening + opt-in session jump buttons + opt-in endless-scroll)
Merge stage-325 to master.
v0.51.30
2026-05-08 14:37:53 -07:00
nesquena-hermes bc4421a1b6 release: v0.51.30 — Release G (3-PR batch: offline recovery + PWA hardening + opt-in session jump buttons + opt-in endless-scroll)
Three-PR contributor batch (all from @ai-ag2026):
- PR #1891: Browser offline recovery + PWA cache hardening
- PR #1928: Opt-in session Start/End jump buttons
- PR #1929: Opt-in session endless-scroll (builds on shipped #1927)

Tests: 4960 → 4977 (+17 net new). Browser API harness all-green.
Manual browser verification on port 8789 passed.
Opus advisor: SHIP-WITH-FIXES (both fast-follows are non-blocking).
2026-05-08 21:31:41 +00:00
nesquena-hermes bec4433c2a Stage 325: PR #1929 — feat: add opt-in session endless scroll by @ai-ag2026
Conflict resolution: both #1928 (session jump buttons) and #1929 (endless
scroll) add their own settings/UI/i18n keys. Resolved by keeping both —
the features are independent opt-in toggles.
2026-05-08 21:23:34 +00:00
nesquena-hermes fba860da48 Stage 325: PR #1928 — feat: add opt-in session jump buttons by @ai-ag2026 2026-05-08 21:16:33 +00:00
nesquena-hermes 503d549cd2 Stage 325: PR #1891 — feat: add browser offline recovery and PWA cache hardening by @ai-ag2026 2026-05-08 21:16:33 +00:00
ai-ag2026 ea8aca2818 feat: add opt-in session endless scroll 2026-05-08 21:16:21 +00:00
ai-ag2026 df1ba9fde8 feat: add opt-in session jump buttons 2026-05-08 21:16:19 +00:00
ai-ag2026 8f58a8c94e feat: add browser offline recovery and PWA cache hardening 2026-05-08 21:16:17 +00:00
nesquena-hermes 596c6b314d Release v0.51.29 — Release F (Docker hardening + login persistence + scroll/lineage fixes + i18n cleanup)
Merge stage-324 to master.
v0.51.29
2026-05-08 14:01:17 -07:00
nesquena-hermes 351fbd3dd2 release: v0.51.29 — Release F (6-PR batch — Docker hardening + login persistence + scroll/lineage fixes + i18n cleanup)
Six-PR contributor batch:
- PR #1919 (franksong2702): Persist login rate limit attempts (closes #1910)
- PR #1920 (franksong2702): Remove dead Kanban start i18n key
- PR #1921 (Michaelyklam): Production Docker image hardening (closes #1908)
- PR #1926 (ai-ag2026): Prevent chat scroll resets after final render
- PR #1927 (ai-ag2026): Preserve viewport when loading older messages
- PR #1930 (ai-ag2026): Collapse stale compression sidebar segments

Tests: 4947 → 4960 (+13 net new). Browser API harness all-green.
Opus advisor: SHIP-READY. CHANGELOG conflict on #1919 auto-resolved
during stage rebase (CHANGELOG took ours strategy).
2026-05-08 20:58:56 +00:00
nesquena-hermes 383507f368 Stage 324: PR #1926 — fix: prevent chat scroll resets after final render by @ai-ag2026 2026-05-08 20:49:00 +00:00
nesquena-hermes 1f8e641e27 Stage 324: PR #1927 — fix: preserve viewport when loading older messages by @ai-ag2026 2026-05-08 20:49:00 +00:00
nesquena-hermes 89b8914704 Stage 324: PR #1930 — fix: collapse stale compression sidebar segments by @ai-ag2026 2026-05-08 20:49:00 +00:00
nesquena-hermes 55fdf48db4 Stage 324: PR #1921 — security: harden production Docker image by @Michaelyklam 2026-05-08 20:49:00 +00:00
nesquena-hermes afb5edff1a Stage 324: PR #1919 — Persist login rate limit attempts by @franksong2702 2026-05-08 20:49:00 +00:00
nesquena-hermes a44fa531ed Stage 324: PR #1920 — Remove dead Kanban start i18n key by @franksong2702 2026-05-08 20:49:00 +00:00
ai-ag2026 447b4e6c0f fix: collapse stale compression sidebar segments 2026-05-08 20:48:47 +00:00
ai-ag2026 018d491570 fix: preserve viewport when loading older messages 2026-05-08 20:48:44 +00:00
ai-ag2026 c65ae46983 fix: prevent chat scroll resets after final render
Keep explicit bottom pins stable across late layout growth and make clicking the already-active sidebar session a no-op before loadSession mutates state. Update scroll regression tests for the delayed settle path.
2026-05-08 20:48:43 +00:00
Frank Song e8fd8dac5d Persist login rate limit attempts 2026-05-08 20:48:41 +00:00
Michael Lam b1b0cedbe9 security: harden production Docker image 2026-05-08 20:48:39 +00:00
Frank Song 431705e498 Remove dead Kanban start i18n key 2026-05-08 20:48:37 +00:00
nesquena-hermes dec2d25fcc Release v0.51.28 — Release E2 (MCP server Option A rewrite + WebUI /goal command)
Merge stage-323 to master.
v0.51.28
2026-05-08 13:28:13 -07:00
nesquena-hermes 0590d597a3 ci: install mcp + pytest-asyncio in CI; importorskip in test_mcp_server.py
CI failed on stage-323 because:
1. mcp_server.py imports the 'mcp' package (optional runtime dep) — only
   users who actually run the MCP integration install it. CI runs with
   stdlib-only deps (pyyaml + pytest + pytest-timeout).
2. tests/test_mcp_server.py uses pytest.mark.asyncio which requires
   pytest-asyncio — not installed in CI.

Fix:
- Add pytest-asyncio to CI install line.
- Try-install mcp; if it fails (Python 3.13 wheel issues, etc.) the test
  module uses pytest.importorskip and skips cleanly without breaking the
  matrix.
- tests/test_mcp_server.py: add module-level importorskip for both 'mcp'
  and 'pytest_asyncio' as a safety net.

Local: 4947/4947 still pass after change.
2026-05-08 20:26:11 +00:00
nesquena-hermes a1d72dc423 release: v0.51.28 — Release E2 (MCP server Option A rewrite + WebUI /goal command)
Two-PR contributor batch:
- PR #1895 (samuelgudi): MCP server Option A rewrite with canonical
  api.models/api.profiles imports, env-aware WEBUI_URL, data-loss
  safety in delete_project. 53-test coverage.
- PR #1866 (Michaelyklam): WebUI /goal command with goal-tracking,
  budget enforcement, continuation prompts. 489-LOC api/goals.py +
  full SSE wire-up.

Tests: 4898 → 4947 (+49 net new). Browser API harness all-green.
Opus advisor: SHIP-READY. Two follow-up items filed for next sweep
(goal-hook firing on unrelated turns; runtime i18n strings).
2026-05-08 20:20:24 +00:00
nesquena-hermes 9655504350 test(mcp_server): restore module identity + fix sys.modules.patch.dict pollution
Root cause: tests/test_mcp_server.py and tests/test_issue1857_usage_overwrite.py
both leaked module state into the full pytest suite, causing 20+ failures in
unrelated test files when they ran together.

Two distinct bugs:

1. test_issue1857_usage_overwrite.py used mock.patch.dict(sys.modules, {...}).
   patch.dict tracks original keys at __enter__ and DELETES any keys added
   during the patch on __exit__. That silently evicted lazily-imported
   pydantic submodules (e.g. pydantic.root_model), producing
   KeyError: 'pydantic.root_model' in test_mcp_server.py downstream.
   Fix: manual save/restore of only the three keys we explicitly inject.

2. test_mcp_server.py mutated module-level constants on api.config / api.models /
   mcp_server (STATE_DIR, SESSION_DIR, PROJECTS_FILE, …) without restoring,
   leaving downstream tests reading deleted tmpdirs. Fix: snapshot original
   values on first _reimport_mcp() call and restore in _cleanup_state_dir.

   Additionally, test_profiles_match_single_source_of_truth re-imported
   api.routes / api.profiles into sys.modules and only restored sys.modules,
   not the parent api package's attributes. `import api.routes as r` resolves
   via sys.modules['api'].routes (parent attribute), NOT directly via
   sys.modules['api.routes']. So fresh modules leaked through despite the
   sys.modules restore. Fix: also restore parent-package attributes.

Result: full pytest suite goes from 20 failures + 36 errors back to all green
(4947 passed, 8 skipped). Up from 4898 in v0.51.27, gain of 49 from
PR #1895 (MCP server tests) + #1866 (goal handler tests).
2026-05-08 19:58:21 +00:00
nesquena-hermes b71a2d4cba Stage 323: PR #1866 — add WebUI /goal command support by @Michaelyklam 2026-05-08 17:40:31 +00:00
nesquena-hermes 92e868cb00 Stage 323: PR #1895 — MCP Option A rewrite — canonical api.models/api.profiles imports by @samuelgudi 2026-05-08 17:12:01 +00:00
Michael Lam 8e513b596b fix: surface goal evaluation status 2026-05-08 17:12:01 +00:00
Samuel Gudi 6fb1c24d60 test(mcp): wire-format coverage + --profile CLI ordering regression (#1895)
Maintainer review on #1895 asked for two test additions:

TestApiWireFormat — stands up a tiny http.server stub on a free port,
points WEBUI_URL at it, and captures (path, body, headers) of every
request the MCP issues:
  - test_rename_session_posts_to_canonical_path: locks /api/session/rename
    URL + body shape so a typo in the path or field names cannot slip
    through validation-only tests.
  - test_move_session_posts_to_canonical_path: same for /api/session/move
    including profile pre-flight against a real local project.
  - test_move_session_unassign_sends_null_project_id: explicit JSON null
    in the body, not an omitted key.
  - test_url_built_from_env_vars: HERMES_WEBUI_HOST/HERMES_WEBUI_PORT
    flow through to WEBUI_URL — would have caught the original 8788 bug.
  - test_url_default_when_env_unset: default 127.0.0.1:8787 matches the
    upstream contract from api/config.py:33.

TestProfileCliOrdering — locks the --profile CLI ordering invariant
(mcp_server.py:62-64): the override of _active_profile must bind before
any consumer reads it. Today this is safe because get_active_profile_name
reads the module global lazily, but a regression that latched the value
at import time would silently make --profile foo a no-op.

50/50 mcp tests pass.

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-08 17:12:01 +00:00
Michael Lam 0db5bc6b76 feat: add WebUI goal command support 2026-05-08 17:12:01 +00:00
Samuel Gudi c613cfa9a7 refactor(profiles): relocate _profiles_match to api/profiles.py (#1895 review)
Maintainer review on PR #1895 flagged that mcp_server.py duplicated the
visibility model from api/routes.py:75. Move the canonical helper into
api/profiles.py (next to _is_root_profile, on which it depends) so both
api/routes.py and mcp_server.py import the same function instead of
carrying parallel definitions that could drift as the model evolves.

- api/profiles.py: + _profiles_match (verbatim from former routes.py:75-97)
- api/routes.py:   replace local definition with re-export to keep all
                   existing _profiles_match(...) call sites resolving
                   without per-call-site refactors
- mcp_server.py:   drop local copy, import _profiles_match alongside the
                   existing api.profiles imports (line 59)
- tests:           + test_profiles_match_single_source_of_truth asserts
                   identity (mcp.module._profiles_match is api.profiles._profiles_match
                   is api.routes._profiles_match) so any re-introduction of
                   a local copy trips the test
                   + test_profiles_match_input_matrix parametrize across
                   the (None|''|'default'|'foo') x (None|''|'default'|'foo'|'bar')
                   visibility matrix per maintainer suggestion

Behaviour unchanged. Zero call-site changes anywhere in api/routes.py.

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-08 17:12:01 +00:00
Samuel Gudi 453f2519f0 fix(mcp): env-aware WEBUI_URL + refuse delete_project unassign without auth
Blocker fixes from maintainer review of #1895.

WEBUI_URL: replace hardcoded 'http://127.0.0.1:8788' with HERMES_WEBUI_HOST/
HERMES_WEBUI_PORT env vars defaulting to 127.0.0.1:8787, mirroring the
contract in api/config.py:32-33. The 8788 default would have failed every
fresh upstream install — 8787 is canonical, 8788 is a local-deployment
quirk on hosts where 8787 is taken by another service.

delete_project no-auth path: remove the filesystem fallback that wrote
session_data['project_id']=None directly via os.replace(). That bypassed
_write_session_index() and left _index.json holding the stale project_id,
causing a running WebUI to keep grouping sessions under the deleted
project until something else triggered a re-compact. Even calling
Session.save() in-process would not have helped because the WebUI's
SESSIONS dict cache lives in a separate process and would overwrite our
update on its next save. The HTTP API is the only cache-safe path —
without auth we now refuse the unassign and surface a 'warning' field.

Tests: + test_delete_no_auth_refuses_unassign locks the new behaviour
(project deleted, sessions and index untouched, warning surfaced).

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-05-08 17:12:00 +00:00