Commit Graph

1065 Commits

Author SHA1 Message Date
nesquena-hermes cc5f6e3a78 Stage 394: PR #2636 2026-05-20 23:53:04 +00:00
nesquena-hermes 45c7a693af Stage 394: PR #2625 2026-05-20 23:53:04 +00:00
nesquena-hermes 38933b288d Stage-394 follow-up: profile-switch reconciliation + a11y switch role + server-side chat/settings filter
Per deep-review verdict SHIP-WITH-FIXES on PR #2636:

1. Profile-switch reconciliation: _refreshProfileSwitchBackground now re-fetches
   /api/settings and re-applies hidden_tabs for the new profile. Without this,
   Profile A's hidden-tabs choice stayed in effect under Profile B until the
   user opened Settings → Appearance.

2. A11y: switched chips from role=button + aria-pressed to role=switch +
   aria-checked. The pressed/not-pressed wording confused screen-reader users
   because chip-off looks like the off state. Added role=group +
   aria-labelledby on the container, and a :focus-visible style on the chips.

3. Server-side belt-and-suspenders: api/config.py now strips 'chat' and
   'settings' from hidden_tabs at validation time, matching the client's apply-
   time filter. A tampered POST can no longer persist the forbidden values.

3 new regression tests added (chat/settings rejection, profile-switch wiring,
chip a11y attributes).

Co-authored-by: FrancescoFarinola <francesco.farinola@example.com>
2026-05-20 23:05:19 +00:00
Francesco Farinola 7f1feca3fe feat: sidebar tab visibility toggle in Settings > Appearance
Add chip row in Settings > Appearance that lets users toggle individual
sidebar/rail tabs on or off. Chat and Settings are always visible.

- Backend: hidden_tabs list setting with validation (no bool coerce)
- Frontend: pill chips that scan rail buttons, autosave via appearance
- Boot: _restoreTabVisibility IIFE applies hidden tabs before first paint
- i18n: 11 locales (label + description)
- Tests: 5 regression tests covering backend, frontend contracts,
  boot restore, i18n coverage, and settings session tracking
2026-05-20 22:57:36 +00:00
nesquena-hermes feb35893b9 Stage 393: PR #2637
# Conflicts:
#	static/sessions.js
2026-05-20 22:24:40 +00:00
nesquena-hermes 4d8b8d0ffe Stage 393: PR #2633
# Conflicts:
#	CHANGELOG.md
2026-05-20 22:23:53 +00:00
nesquena-hermes e35c94bf55 Stage 393: PR #2615 2026-05-20 22:23:53 +00:00
nesquena-hermes aaf30b7b0a Stage 392: PR #2643 2026-05-20 21:48:04 +00:00
nesquena-hermes fa459aa01e Stage 392: PR #2651 2026-05-20 21:48:04 +00:00
nesquena-hermes b4a00b5aae Stage 392: PR #2650 2026-05-20 21:48:04 +00:00
nesquena-hermes dc0c833744 Stage 392: PR #2647 2026-05-20 21:48:04 +00:00
nesquena-hermes 6ed66daac2 Stage 392: PR #2638 2026-05-20 21:48:04 +00:00
Lumen Yang 71fbc796b2 fix: dedupe replayed context tail after compression 2026-05-20 23:15:54 +02:00
dobby-d-elf 6278222596 tighten session refresh invalidation 2026-05-20 14:40:13 -06:00
starship-s 153e035d12 fix: forward title generation api key 2026-05-20 14:39:38 -06:00
dobby-d-elf 14dd5aa00d address session event review 2026-05-20 14:33:36 -06:00
Colin Chang 9c3e37d2ee fix: custom_providers models allowlist takes priority over live /v1/models fetch
Custom providers that have a curated models: list in config.yaml
(e.g. ZenMux gateways) should show ONLY those configured models in
the picker dropdown, not the full /v1/models catalog.

Before this fix, _named_custom_groups unconditionally called
_read_custom_endpoint_models() which would pull hundreds of models
from aggregator gateways and overwrite the user's curated list.

Now the build checks if the custom_provider entry has a non-empty
models dict/list in config.yaml — if so, it skips the live fetch
and uses only the configured models (same behavior as hermes-agent
model_switch.py Section 4 patch).

Closes: configure-model-list-should-be-authoritative
2026-05-20 20:22:11 +00:00
dobby-d-elf 87527ff4f6 Fix state db legacy dedup repeat preservation 2026-05-20 14:18:47 -06:00
nesquena-hermes 1e3ca07575 Stage 390: PR #2634
# Conflicts:
#	CHANGELOG.md
2026-05-20 20:16:30 +00:00
nesquena-hermes 495991c2db Stage 390: PR #2642 2026-05-20 20:16:30 +00:00
Arsh Kumar Singh 2253cf5a32 chore: address review notes — dedup comment and 409-path clarification 2026-05-20 19:57:20 +00:00
Michael Lam 6e64068f0f fix: cap CLI session sidebar state scans 2026-05-20 12:47:03 -07:00
nesquena-hermes 7c2d56c920 Stage 389 follow-up: close TOCTOU race in pin-cap (Opus advisor #2614) 2026-05-20 18:12:38 +00:00
dobby-d-elf 19ad20afff Fix new chats using profile default model 2026-05-20 10:57:04 -06:00
nesquena-hermes dd36d09f89 Stage 389: PR #2626
# Conflicts:
#	CHANGELOG.md
2026-05-20 16:41:45 +00:00
nesquena-hermes 3d34eef02d Stage 389: PR #2620 2026-05-20 16:41:45 +00:00
nesquena-hermes 4d8e1ccc10 Stage 389: PR #2618 2026-05-20 16:41:44 +00:00
nesquena-hermes eaff4d0b8e Stage 389: PR #2614
# Conflicts:
#	CHANGELOG.md
2026-05-20 16:41:44 +00:00
nesquena-hermes 3bcd81b79f Stage 389: PR #2612
# Conflicts:
#	CHANGELOG.md
2026-05-20 16:41:44 +00:00
Arsh Kumar Singh d385db69d5 fix(clarify): require stable clarify_id and wait for backend ack so stale responses are rejected
The WebUI clarification popup had a response-delivery failure: users
submitted answers in the popup, but the agent still fell through to the
timeout fallback message.  Three bugs conspired:

1. No stable clarify_id — _ClarifyEntry had no unique identifier, so
   the frontend could not reference a specific pending prompt.  The
   backend used FIFO resolution which silently failed for stale/late
   responses.

2. Frontend hid the card before confirmation — respondClarify() called
   hideClarifyCard(true, 'sent') BEFORE the API call completed.  If the
   backend rejected the response, the card was already gone and the
   user's draft was discarded.

3. Backend lied about success — _resolve_clarify_legacy() returned
   bool(resolved) or not bool(clarify_id).  Since the frontend never
   sent clarify_id, the backend always reported ok:true even when
   nothing was resolved.

Changes:

api/clarify.py:
- _ClarifyEntry now auto-generates a stable clarify_id (uuid4.hex[:12])
- submit_pending() injects clarify_id into the data dict visible to the
  frontend via SSE and polling
- New resolve_clarify_by_id() for O(1) lookup by id instead of FIFO pop

api/routes.py:
- _resolve_clarify_legacy() uses resolve_clarify_by_id when clarify_id
  is provided; returns actual bool result (no more unconditional True)
- _handle_clarify_respond() returns HTTP 409 + {ok:false, stale:true}
  when resolution fails

static/messages.js:
- respondClarify() now sends clarify_id in the POST body
- Waits for a positive backend acknowledgement before hiding the card
- Saves a draft copy before POST and restores it on failure
- On 409/network error: re-enables controls, shows error toast
- Guards against parallel-SSE race where clearing the cache after a
  successful response could erase a newly queued next prompt (codex P1)

tests:
- Updated test_sprint30.py for new ack-before-hide behaviour
- Updated test_clarify_unblock.py for 409 on stale responses

Closes #2639.
2026-05-20 16:35:15 +00:00
Dennis Soong cec435a833 fix(session): rebuild missing startup index 2026-05-20 23:43:30 +08:00
dobby-d-elf fd7212b014 Optimize profile switching and session list loading 2026-05-20 08:47:49 -06:00
dobby-d-elf 5e378d3b38 sync session list from server events 2026-05-20 08:18:56 -06:00
Michael Lam 8ef8fae831 fix: show config-managed custom providers 2026-05-20 06:27:00 -07:00
Isla Liu 2a303de2a3 fix(session): preserve retry budget while journal is still arriving 2026-05-20 20:55:07 +08:00
Isla Liu d5a185d9c6 fix(session): serialize lazy journal retry per session 2026-05-20 20:48:38 +08:00
stocky789 9ac94d3ef6 fix(workspace): tighten git subprocess trust boundary 2026-05-20 11:02:45 +00:00
Michael Lam c3eafa34f8 fix: surface custom provider model endpoint errors 2026-05-20 03:12:33 -07:00
stocky789 898e15a899 fix(workspace): restore branch changes on switch 2026-05-20 08:14:30 +00:00
manji ff0aa69d5f fix(session): use second-level timestamp granularity in legacy dedup key
The _normalized_message_timestamp_for_key helper was preserving
microsecond precision (%.6f). When the same message is persisted by
both the WebUI sidecar JSON writer and the Hermes agent state.db
writer, their timestamps can differ by a few microseconds, causing
_session_message_merge_key to produce different keys for the same
logical message and letting both copies survive the dedup pass in
merge_session_messages_append_only.

Truncating to second-level granularity collapses sub-second drift to
the same key, so the duplicate is suppressed correctly.

Fixes #2616
2026-05-20 07:13:55 +00:00
stocky789 0f9c64b780 fix: classify CRLF-only git status noise
Distinguish CRLF-only working tree changes from filemode-only noise when the ignored-CR diff path set is empty on GitHub Actions.
2026-05-20 05:43:17 +00:00
Lumen Yang b2c6af12f1 fix(webui): prefer sidecar counts over stale session index 2026-05-20 05:42:55 +00:00
stocky789 5fc7aee781 feat(workspace): add backend Git operations 2026-05-20 04:51:41 +00:00
Isla Liu 9870e8f111 fix(session): address Copilot review — scope tool-card dedupe by stream id + tighten docs
Four code-review comments from the automated Copilot reviewer on this PR:

1. `_journal_tool_already_present` dedupe was session-wide, so a
   legitimately-repeated tool (e.g. a second `terminal: ls` in an
   earlier turn) could cause the retry path to falsely skip
   materializing the recovered tool card.  The helper now takes a
   keyword `stream_id` argument; when supplied, a tool card whose
   `_recovered_stream_id` is set AND differs from the candidate is no
   longer treated as a duplicate.  Untagged tool cards (live tools, or
   tool cards carried over from a pre-tagging core transcript) still
   match, preserving the existing 'core transcript already has this
   tool, don't duplicate' invariant.  Two new tests in
   `TestJournalToolDedupeScoping` cover both legs of the rule.

2./3. The troubleshooting FAQ pointed at `~/.hermes/webui/sessions/session_<sid>.json`
   and `~/.hermes/_run_journal/...`.  The actual sidecar filename has
   no `session_` prefix and the run-journal lives under the WebUI
   sessions dir (`~/.hermes/webui/sessions/_run_journal/<sid>/<stream>.jsonl`,
   default).  Both paths fixed and an explicit note added about
   `HERMES_WEBUI_STATE_DIR` overriding the state root.

4. Drop unused `json` / `queue` / `Path` imports from
   `tests/test_session_lost_response_regression.py` so the file stops
   carrying noise that future linting would flag.
2026-05-20 12:18:03 +08:00
Isla Liu 75a26174aa fix(session): lazily retry run-journal recovery so the interrupted-turn marker self-heals
When the WebUI process restarts mid-stream and sidecar repair runs while
the run-journal for the dead stream is not yet visible on disk (WSL2 9p
/ DrvFs page-cache loss, un-fsynced journal tail on network FS, …),
`_append_journaled_partial_output()` returns False and the marker is
permanently baked with the "no agent output was recovered" wording even
though the journaled tokens appear on disk shortly afterwards.

This commit reframes the recovery contract so the read side can
self-heal:

  * `_interrupted_recovery_marker` gains a `pending_retry=True` mode
    that produces a third wording ("Recovering the partial output …
    reload this session to retry.") and stamps a
    `_pending_journal_recovery` flag.
  * `_apply_core_sync_or_error_marker` now writes that pending-retry
    marker (with `_journal_retry_stream_id`,
    `_journal_retry_attempts`, `_journal_retry_first_seen_ts` meta)
    whenever it cannot recover visible output AND the stream id is
    known. The legacy "no output" wording is reserved for the
    no-stream-id case. The core-sync branch leaves marker emission to
    the existing visible-output check (the core transcript itself is the
    canonical history in that branch).
  * A new `_retry_journal_recovery_in_place(session)` helper re-runs
    `_append_journaled_partial_output(…, dedupe_existing=True)` for the
    latest pending marker. On success the marker is promoted in place to
    the recovered-output wording, the journaled rows are reordered to
    sit above the marker (preserving chronological order), and all
    retry meta is stripped. On failure attempts is incremented; after
    _JOURNAL_RETRY_MAX_ATTEMPTS (12) or _JOURNAL_RETRY_GIVEUP_SECONDS
    (24h) the marker is demoted to a neutral "Partial output may have
    been lost." wording.
  * `get_session()` cheaply short-circuits via
    `_session_has_pending_journal_retry()` and invokes the helper on
    both cache-hit and cold-load paths when a pending marker is found.
    `metadata_only=True` skips the helper to keep sidebar refresh
    cheap. The retry call runs OUTSIDE the SESSIONS LOCK to avoid a
    deadlock with `session.save()` write paths.

No streaming write path or run_journal fsync behaviour is changed — the
fix is read-side only.
2026-05-20 11:58:26 +08:00
Michael Lam 9ca846eb8f feat: cap pinned sessions from sidebar 2026-05-19 20:50:26 -07:00
AJV20 8109bcb7e4 fix: honor configured title generation model 2026-05-19 22:35:11 -04:00
AJV20 f966e99231 fix: preserve IPv6 dashboard link brackets 2026-05-19 21:35:10 -04:00
nesquena-hermes ed6ee3e067 Stage 388: PR #2607
# Conflicts:
#	CHANGELOG.md
2026-05-20 00:17:48 +00:00
nesquena-hermes a201401236 Stage 388: PR #2524 2026-05-20 00:17:48 +00:00