Commit graph

4720 commits

Author SHA1 Message Date
dobby-d-elf
19ad20afff Fix new chats using profile default model 2026-05-20 10:57:04 -06:00
nesquena-hermes
2b5a960df2 Stamp CHANGELOG for v0.51.96 (Release BT / stage-389 / 8-PR batch) 2026-05-20 16:43:15 +00:00
nesquena-hermes
360a57164a Stage 389: PR #2627
# Conflicts:
#	CHANGELOG.md
2026-05-20 16:41:45 +00: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
84f6bf5323 Stage 389: PR #2619
# Conflicts:
#	CHANGELOG.md
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
nesquena-hermes
9c564ccc1b Stage 389: PR #2610 2026-05-20 16:40:42 +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
Michael Lam
6eb5d939d7 test: allow custom provider settings filter 2026-05-20 09:33:51 -07:00
dobby-d-elf
dc57d358eb fix test 2026-05-20 10:02:46 -06:00
dobby-d-elf
439a574cf4 address PR comments & bugs from real testing 2026-05-20 09:59:36 -06: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
4c8914304b fix: keep compact tool activity grouped
Compact tool activity regressed into separate Activity rows and standalone Thinking blurbs when interim assistant text retired the current live activity group and Thinking rendered outside the disclosure.

Render Compact-mode Thinking inside the shared Activity body for live and settled turns, keep interim assistant text from splitting the current Activity group, and remove the now-unused stream-local activity-close path. This restores the intended single compact disclosure without adding new functionality.
2026-05-20 08:29:46 -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
98106c809b docs(session): clarify lazy retry trigger for metadata-only polling 2026-05-20 20:55:08 +08:00
Isla Liu
37c3e84ad2 test(session): cover lazy journal retry give-up paths 2026-05-20 20:55:08 +08: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
Michael Lam
680d0cbc92 docs(runtime): define runner backend harness gate 2026-05-20 04:05:36 -07: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
Michael Lam
471b75d762 docs: move Hermes overview out of agent context root 2026-05-19 23:55:58 -07: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
Isla Liu
1957785332 fix(session): address Copilot round-2 review — correct stale comment and drop unused fixture arg
Two non-functional cleanups from the second Copilot pass:

1. The inline comment in `test_error_marker_no_preserved_as_draft`
   said the legacy "user message above was preserved" wording was used
   for the post-retry-give-up case.  The actual implementation demotes
   give-up markers to a different neutral wording ("Partial output may
   have been lost.").  Comment rewritten to match the contract.

2. The regression test `test_lost_response_recovered_on_second_read`
   declared a `monkeypatch` parameter it never used.  Dropped.
2026-05-20 13:08:08 +08: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
Mark Baker
a2ce4e81b5
fix(plugins): distinguish exclusive/provider activation in Settings panel
The Settings → Plugins panel keyed off `loaded.enabled` and the four
agentic visibility hooks, both of which are False/empty for exclusive
plugins (memory.provider, model-provider, etc.). Those plugins were
mislabeled as "Disabled" with "No registered lifecycle hooks" even when
fully functional as the active provider for their category.

Surface `manifest.kind` and a derived `activation` field
("enabled" | "disabled" | "exclusive" | "provider") in /api/plugins.
The card render picks a third badge state ("Active (provider)") and a
dedicated empty-hooks line for those rows. `enabled` is preserved in
the payload so older clients still work; new clients should prefer
`activation`.

Fixes #2659
2026-05-20 00:01:02 -04:00
Isla Liu
66b6d8f019 docs(session): CHANGELOG entry + troubleshooting FAQ for the lost-response self-heal
CHANGELOG: append an Unreleased / Fixed entry describing the user-visible
behaviour change (interrupted-turn marker now self-heals on the next
session read; gives up gracefully after 12 retries or 24h).

docs/troubleshooting.md: add a 'Symptom → Why → Diagnostic → Fix →
Caps → When to file a bug' entry for the
'no agent output was recovered' marker so users who hit the lost-response
shape on WSL2 / network FS can recognise it, verify the run-journal on
disk, and know that reloading the session is enough.
2026-05-20 11:59:06 +08:00
Isla Liu
2387720068 test(session): unit + backwards-compat + WSL race coverage for lazy-retry path
Adds five test classes that together pin down the contract added in the
previous commit and protect pre-fix session shapes:

  * `TestInterruptedRecoveryMarker` — pure-function tests for the new
    `pending_retry=True` keyword and the mutual-exclusion rule between
    `recovered_output=True` and `pending_retry=True`.
  * `TestRetryJournalRecoveryInPlace` — promote-on-success,
    increment-on-failure, demote-after-max-attempts,
    demote-after-giveup-seconds, no-op when no pending marker, and the
    `_session_has_pending_journal_retry` short-circuit (which stops at
    the most recent normal assistant turn).
  * `TestGetSessionLazyRetryHook` — both `get_session()` entry paths
    (cache-hit and cold-load) trigger the helper when a pending marker
    is present; the short-circuit avoids the helper when nothing is
    pending; and `metadata_only=True` skips the helper to keep sidebar
    refresh free.
  * `TestLazyRetryBackwardsCompat` — pre-fix sessions whose markers
    use the legacy "no agent output" wording (no flag) are not touched
    by `get_session()`. The four retry-meta keys round-trip cleanly
    through `Session.save()` / `Session.load()`.
  * `TestWslPageCacheRace` — covers the WSL2 / network-FS shape: a
    first `read_run_events` raising IOError followed by a successful
    read; a journal that grows visible tokens between sidecar repair
    and retry; and two concurrent `get_session(sid)` calls converging
    on a single promoted marker with a single recovered body
    (deduped by `dedupe_existing=True`).

Two pre-existing assertions had to be relaxed because they encoded the
buggy contract (permanent "no agent output was recovered" / "user
message above was preserved" wording in the journal-empty + stream-id
known case). Both tests now accept either the legacy wording or the
new "Recovering the partial output…" wording with the pending-retry
flag, reflecting the broader fact that the old wording was the bug.
2026-05-20 11:58:54 +08:00
Isla Liu
e8cd0bcc66 test(session): end-to-end regression for lost-response self-heal
Reproduces the production failure mode:

1. Stage 1 — sidecar repair runs while the run-journal for the dead
   stream is empty on disk. Assert the marker arms the lazy-retry
   hook (`_pending_journal_recovery=True`,
   `_journal_retry_stream_id`, `_journal_retry_attempts=0`,
   `_journal_retry_first_seen_ts`) and does NOT carry the legacy
   "no agent output was recovered" wording. Pending sidecar fields
   are cleared regardless.

2. Stage 2 — journaled token / tool / tool_complete / token events
   appear on disk. Call `get_session(sid)` and assert the marker
   self-heals: wording promotes to "recovered from the run journal",
   journaled assistant rows + tool card land above the marker in
   chronological order, all retry meta is stripped.

Without the lazy-retry path this test fails at the very first
assertion (marker still carries the legacy no-output wording).
2026-05-20 11:58:37 +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
9c983e693a
Merge pull request #2608 from nesquena/release/stage-388
release: v0.51.95 (Release BS / stage-388 / 5-PR batch — live tool dedup + browser dashboard links + messaging dedup + Geist Contrast skin + SSE diagnostics)
2026-05-19 17:40:44 -07:00
nesquena-hermes
7eccff4fed Stamp CHANGELOG for v0.51.95 (Release BS / stage-388 / 5-PR batch) 2026-05-20 00:37:35 +00:00
nesquena-hermes
fc5639db15 test: exempt skin-specific scoped overrides from session-title color assertion
PR #2521 (Geist Contrast skin) adds a scoped
`:root[data-skin="geist-contrast"] .session-item.active .session-title` rule
that legitimately uses its own palette values. The existing assertion in
test_sprint40_ui_polish.py matched on any line containing the
`.session-item.active .session-title` substring, picking up the skin-scoped
override and asserting against its palette.

Exclude lines containing `:root[data-skin=` from the base-rule scan so
skin-scoped overrides are free to use their own design tokens, while the
global rule still enforces var(--gold) / var(--accent-text).
2026-05-20 00:26:11 +00:00
nesquena-hermes
1a8c7b65c9 test: anchor picker-active-state assertions to global #mainSettings rule
PR #2521 (Geist Contrast skin) legitimately adds a scoped
`:root[data-skin="geist-contrast"] .theme-pick-btn.active` override that
appears earlier in style.css than the global `#mainSettings .theme-pick-btn.active`
rule. The naive substring search in tests/test_1059_settings_picker_active_state.py
found the skin-specific override first (which correctly uses --border2 for its
palette), failing the global assertion that wanted --accent.

Tighten both assertions to anchor on the `#mainSettings` selector prefix so
they always match the global rule regardless of how many skin-specific
overrides land in the file.
2026-05-20 00:23:01 +00:00
nesquena-hermes
b2c9bddadf Stamp CHANGELOG for stage-388 (Release BS — 5-PR batch)
Unreleased section now reflects:
- PR #2598 live tool event dedup (AJV20)
- PR #2533 browser dashboard links (AJV20)
- PR #2607 messaging transcript dedup (AJV20)
- PR #2521 Geist Contrast skin (intellectronica)
- PR #2524 SSE runtime diagnostics endpoint (AJV20)

Removed merge markers and consolidated stray entries that leaked into the v0.51.94 release block.
2026-05-20 00:20:55 +00: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
nesquena-hermes
bd819f5e68 Stage 388: PR #2533 2026-05-20 00:17:47 +00:00