Commit Graph

1712 Commits

Author SHA1 Message Date
dobby-d-elf 1cf0ff01b5 feat: live context window status tracking during streaming 2026-05-10 06:51:46 -06:00
nesquena-hermes 8a653bac20 Merge pull request #1967 from nesquena/stage-326
release: v0.51.31 — Release H (12-PR contributor batch: image-mode + race fixes + composer drafts + locale parity)
v0.51.31
2026-05-09 11:55:08 -07:00
nesquena-hermes 1d7344c602 release: v0.51.31 — Release H (12-PR contributor batch)
CHANGELOG, ROADMAP, TESTING refresh for v0.51.31 stage release covering
12 contributor PRs:

Added (2 PRs):
- #1956 JKJameson — persistent composer draft (server-side, cross-client)
- #1957 hermes-gimmethebeans — configurable session TTL via env + settings

Fixed (10 PRs):
- #1939 ai-ag2026 — theme-color + sw cache regression coverage
- #1941 ai-ag2026 — preserve chat scroll across final render
- #1945 franksong2702 — localize session jump controls (#1938)
- #1947 happy5318 — show same model from different custom providers
  (Co-authored-by hacker1e7 for #1874 close)
- #1949 Sanjays2402 — close #1937 endless-scroll vs Start-jump race
  with generation-token + mutex
  (Co-authored-by franksong2702 + Michaelyklam)
- #1950 franksong2702 — mute stale stopped gateway heartbeat (#1944)
- #1951 amlyczz — gate goal hook on goal-related turns (#1932)
  (Co-authored-by franksong2702 for #1946 close)
- #1953 lucky-yonug — skip provider peel for custom host:port slugs
- #1960 Michaelyklam — translate hidden-files workspace label (#1841)
- #1961 sbe27 — respect image_input_mode (#1959)

Closed in favor of canonical: #1942, #1962, #1946, #1874, #1311.

Stage-326 hotfixes (per Opus advisor):
- CRITICAL #1951 PENDING_GOAL_CONTINUATION race fix (removed finally
  discard that race-erased the marker before consumer could read it)
- #1956 composer-draft input validation (50 KB text / 50 file clamp +
  type coercion to prevent unbounded session-JSON bloat)
- #1957 SESSION_TTL constant preserved as named fallback (existing
  regression tests pin it; #1957 originally deleted it)

Tests: 5006 → 5028 (+51 net new) — 0 regressions, 142.61s runtime.
2026-05-09 18:46:25 +00:00
nesquena-hermes 8782fd2675 fix(stage-326): apply Opus advisor critical + recommended fixes
CRITICAL: #1951 PENDING_GOAL_CONTINUATION race
  Removes `PENDING_GOAL_CONTINUATION.discard(session_id)` from the
  streaming worker's `finally` cleanup block. The marker is set inside
  the SAME function call (line ~3328 on `goal_continue`) and the discard
  in the `finally` (line ~3553) almost always raced ahead of the
  frontend's SSE-receive → POST /api/chat/start round-trip, erasing
  the marker before the consumer in routes.py could read it. The
  consumer (`_start_chat_stream_for_session` in routes.py:6522) already
  discards atomically when consuming, so removing the streaming-side
  discard preserves single-use semantics and unblocks the
  goal-continuation chain.

  Adds tests/test_stage326_pending_goal_continuation_race.py with 5
  regression guards:
  1. streaming.py's finally must NOT discard PENDING_GOAL_CONTINUATION
  2. routes.py consumer must check + set + discard atomically
  3. PENDING_GOAL_CONTINUATION must be a set (GIL-safe single-op)
  4. STREAM_GOAL_RELATED.pop must be keyed by stream_id, not session_id
  5. PENDING_GOAL_CONTINUATION.add must precede the goal_continue SSE
     emission in source ordering

HARDENING: #1956 composer-draft input validation
  Per Opus, the POST /api/session/draft handler accepted unbounded /
  arbitrary-typed text and files inputs. With the 400ms debounced
  auto-save firing on every keystroke, a misbehaving client could
  persist multi-MB strings into the session JSON. Adds:
  - text: coerced to str if not already; clamped to 50_000 chars
  - files: coerced to list if not already; clamped to 50 entries
  Validation runs BEFORE the session lock acquire / save.

  Adds tests/test_stage326_composer_draft_validation.py with 5 guards.

Verdict from Opus advisor on stage-326: SHIP-WITH-FIXES.
This commit applies the required + recommended fixes; #1957 hardening
fixed in a prior stage commit.
2026-05-09 18:36:01 +00:00
nesquena-hermes 404e24ac9d fix(stage-326): preserve SESSION_TTL constant + reconcile #1957 tests
PR #1957 deleted the SESSION_TTL = 86400 * 30 module-level constant in
favor of the new _resolve_session_ttl() helper. Two existing regression
tests pin the constant: test_auth_sessions.TestSessionPruning.test_session_ttl_is_24_hours
imports SESSION_TTL directly, and test_v050258_opus_followups.test_redirect_session_ttl_30_days
asserts the literal "SESSION_TTL = 86400 * 30" line is present in source
(guarding against the daily-kick-out regression from #1419).

Restore SESSION_TTL as the named fallback for _resolve_session_ttl(); the
new env-var/settings.json path is unchanged. Backwards-compatible.

Also fix the new TestSessionTtlResolution suite:
- Switch from pytest's `monkeypatch` fixture (incompatible with
  unittest.TestCase subclasses) to setUp/tearDown env snapshotting
- Reconcile clamp tests with actual implementation: out-of-range env
  values fall through to settings/default, not snap to bounds
- test_session_uses_dynamic_ttl now sets the env var so the dynamic
  resolved value (3600s) is exercised rather than expecting the default

Verified: tests/test_auth_sessions.py + tests/test_v050258_opus_followups.py
21/21 pass.
2026-05-09 18:33:28 +00:00
nesquena-hermes 7cf8dcff4c Stage 326: PR #1956 — feat: persistent composer draft — server-side, cross-client, survives refresh by @JKJameson 2026-05-09 18:17:51 +00:00
nesquena-hermes 07d39612ce Stage 326: PR #1949 — fix(#1937): close endless-scroll prefetch vs Start-jump race with generation-token + mutex by @Sanjays2402
# Conflicts:
#	CHANGELOG.md
2026-05-09 18:17:51 +00:00
nesquena-hermes 4751b5ace5 Stage 326: PR #1951 — fix: only evaluate goal hook on goal-related turns (#1932) by @amlyczz 2026-05-09 18:17:20 +00:00
nesquena-hermes a0a65ba0bc Stage 326: PR #1941 — fix: preserve chat scroll across final render by @ai-ag2026 2026-05-09 18:17:20 +00:00
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
Minimax 08c4ef8d88 feat: persistent composer draft — server-side, cross-client, survives refresh
- Session.composer_draft field: {text, files} stored in session JSON
- POST+GET /api/session/draft endpoint for save/load
- loadSession: save draft before switch, restore from S.session.composer_draft
- textarea input: debounced 400ms auto-save to server
- send(): clear draft after message is sent
- lockComposerForClarify(): save draft before card locks composer
- _restoreComposerDraft: clears textarea when target has no draft, guards
  against stale responses racing new session loads, exact text comparison
- Session.compact(): includes composer_draft in response
- Fix: use handler.command instead of parsed.method (ParseResult has no .method)

Co-authored-by: Minimax <noreply@minimax.io>
2026-05-09 13:47:57 +01: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
zqy 6fd07c2af4 fix: only evaluate goal hook on goal-related turns (#1932)
The goal evaluation hook was firing on every completed assistant turn
when a goal was active, even for unrelated messages like "what time is
it". This burned the goal budget, triggered continuation prompts that
interrupted unrelated conversations, and made /goal status numbers
misleading.

Add STREAM_GOAL_RELATED and PENDING_GOAL_CONTINUATION flags to gate
the evaluate_goal_after_turn() call in the streaming loop. Only streams
started from goal kickoff (/goal <text>) or goal continuation are
marked as goal-related. Normal user messages skip the hook entirely.
2026-05-09 15:08:13 +08:00
Frank Song b38cc2f1ea Mute stale stopped gateway heartbeat 2026-05-09 14:53:42 +08:00
Sanjay Santhanam fb822239ea fix(#1937): close endless-scroll prefetch vs Start-jump race with generation-token + mutex
The originally-proposed fix (gate _ensureAllMessagesLoaded on the existing
_loadingOlder flag) does not actually close the race. By the time the
prefetch reaches its post-await body, it has already cleared the entry-
gate that reads _loadingOlder, so a same-flag check inside the resolved
callback would be a no-op for an in-flight request.

The actual fix is two-pronged:

1. New module-scoped _messagesGeneration counter, bumped every time
   S.messages is wholesale-replaced. _loadOlderMessages snapshots it
   BEFORE its await and re-checks after — if it changed, the prepend
   is aborted. This is the canonical async-invalidation pattern.

2. _ensureAllMessagesLoaded now claims the _loadingOlder mutex around
   its body so a new prefetch cannot start mid-replace and concurrent
   ensure-all calls (rapid double-click on Start) serialize cleanly.
   It bumps the generation token before mutating S.messages, yields
   until any in-flight prefetch finishes, and resets _oldestIdx so a
   subsequent prefetch cannot request stale older messages.

Also adds the same-session / _loadingSessionId guards that the original
ensure-all body was missing post-await — if the user switched sessions
mid-flight, the old code would happily overwrite the new session's
messages with the previous session's full history.

12 new regression tests in tests/test_issue1937_endless_scroll_jumpstart_race.py
lock in: generation token declaration, bump-helper presence, snapshot-
before-await ordering, post-await-abort behaviour, mutex acquisition and
finally-release, yield-then-claim ordering when a prefetch is in flight,
generation bump during the wait phase, _oldestIdx reset, and the new
session-switch guard.

Closes #1937.
2026-05-08 21:14:22 -07:00
Frank Song 3dfd692d75 Localize session jump controls 2026-05-09 10:03:27 +08:00
ai-ag2026 d84eaea594 ci: retrigger flaky ctl test 2026-05-09 02:19:32 +02:00
ai-ag2026 1559c70a41 fix: preserve chat scroll across final render 2026-05-09 02:15:35 +02: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