Commit Graph
100 Commits
Author SHA1 Message Date
nesquena-hermes fa5ff8414e docs(changelog): de-flake test_skills_stats_cache mtime race (#5178) 2026-06-29 06:08:52 +00:00
nesquena-hermes 359eae93de test: de-flake test_skills_stats_cache mtime race
test_skills_stats_cache asserted that adding a skill bumps the skills-dir
mtime so the cheap per-call probe recomputes counts on the next call. Two
_write_skill() calls inside the same coarse filesystem mtime tick could leave
the skills-dir mtime byte-identical to the cached probe value, so the probe
saw no change and served the stale count of 1 (assert enabled==2 failed). This
is an order-dependent flake: passes in isolation, fails intermittently under
full-suite ordering (observed across multiple unrelated PRs).

Force the skills-dir mtime strictly forward after the second write so the
probe is guaranteed to detect a genuine mtime change. The assertion stays
honest (it still proves recompute-on-mtime-change); only the race is removed.
2026-06-29 06:07:44 +00:00
nesquena-hermes 72f7530c45 ci: run tests across 5 shards (up from 3) + fix 2 latent test-isolation leaks
Two cross-file isolation leaks that a finer shard partition exposes are fixed at
root so every shard passes regardless of execution order:
- test_auth_session_persistence.py: bind api.auth STATE_DIR/_SESSIONS_FILE/
  _LOGIN_ATTEMPTS_FILE in setUp + restore in tearDown (was relying on a sibling
  reload test to rebind them).
- test_request_diagnostics_cache.py: pin the session-list cache source stamp via
  an autouse fixture (matches test_session_sidebar_cache.py) so a leaked state.db
  WAL sidecar can't cause a spurious cache-miss.

Verified all shards green under 3-, 4-, and 5-way partitions. CI infra + test
harness only; no application behavior change.
2026-06-29 05:55:16 +00:00
nesquena-hermes 744b6480cf docs(changelog): runtime-journal anchor-scene parity regression test (#5183) 2026-06-29 05:37:02 +00:00
nesquena-hermes 2d71598f16 docs(changelog): authorize session-backed HTML/PDF media previews (#5157) 2026-06-29 05:18:53 +00:00
nesquena-hermes c998a316ed docs(changelog): align Workspace Todos rendering with sidebar (#5103) 2026-06-29 05:13:02 +00:00
nesquena-hermes c59be15206 docs(changelog): resolve git under macOS launchd (#5184, #5175) 2026-06-29 04:36:10 +00:00
nesquena-hermes 793b4eef8c docs(changelog): settings-search coverage (#5163) + Windows venv test.sh (#5166) 2026-06-29 04:16:57 +00:00
nesquena-hermes fae5c45395 Merge #5163 into stage/p1a 2026-06-29 04:07:21 +00:00
nesquena-hermes 65b27886d0 Merge #5166 into stage/p1a 2026-06-29 04:07:21 +00:00
nesquena-hermes 9e642b1331 docs(changelog): restart gateway after in-app agent update (#5181, #5156) 2026-06-29 03:53:48 +00:00
nesquena-hermesandPaladin173 ca06796764 fix(i18n): restore default-off quota-chip copy (Codex gate #4657)
Codex found leftover quota-chip residue: #4657 had rewritten settings_desc_quota_chip
to 'Shown by default...' (and regressed several locales to English), contradicting
the reverted default-OFF behavior. Restored master's localized 'Default off / wide
displays only' copy across all 14 locales + index.html. The composer_mobile_quota
strings (mobile config panel, fit-related) are kept.

Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 02:06:15 +00:00
nesquena-hermesandPaladin173 628d378857 chore(composer): drop #4657's quota-chip default-flip — keep just the fit-engine
Per maintainer decision: #4657 bundled a scope-creep change flipping
show_quota_chip default off->on plus a migration force-resetting stored false->true
(stomping explicit opt-outs). Master deliberately ships quota-chip default-off.
Reverted config.py + the quota-chip wiring in panels.js/boot.js + the quota test
to master, keeping ONLY the footer fit-by-overflow engine. The mobile quota-action
selector addition (hide_composer_quota_chip selectors) is kept (fit-related, not a
default change).

Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:38:34 +00:00
nesquena-hermesandPaladin173 6d770f1c05 fix(composer): refit on .composer-left resize, not just footer (Codex gate #4657)
Codex flagged that the fit ResizeObserver only watched .composer-footer, whose
outer width may not change when right-side controls (status/context chips)
appear — leaving the left control group's fit stale. Also observe .composer-left.

Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:33:26 +00:00
nesquena-hermes d0be66f2d1 docs(changelog): composer footer fit-by-overflow (#4657) 2026-06-29 01:19:34 +00:00
nesquena-hermes c6370235be feat(composer): fit footer controls by overflow (#4657, rebased onto master)
Rebased Paladin173's #4657 onto current master. The per-control visibility
toggles half already shipped via his own #4598, so this rebase carries only the
net-new JS fit-by-overflow engine (_fitComposerFooter: ResizeObserver +
MutationObserver measure .composer-left overflow, step full -> cf-icons ->
cf-burger). Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:19:02 +00:00
nesquena-hermes c344844766 fix(#5132): split state.db sidebar override — source uncapped, count capped
Codex deep-gate caught a CORE regression in the original top-N cap: capping
_apply_sidebar_state_db_overrides also capped the SOURCE classification that
/api/sessions filters on BEFORE the lazy lineage correction, so a stale-CLI JSON
row whose state.db source is webui could be silently filtered out of a WebUI-only
sidebar once it fell beyond the cap. Split the read: the indexed sessions-table
source/title lookup runs for ALL rows; only the expensive messages COUNT/MAX
aggregation (the actual 5-18s bottleneck) is capped to top-N. Added a 500-row
end-to-end regression with a real state.db proving the beyond-cap stale-CLI row
stays webui.
2026-06-29 00:54:15 +00:00
nesquena-hermesandlatipun c646d92206 perf(#5132): cap /api/sessions state.db source/title overrides to top-N
On power-user instances with thousands of sessions, GET /api/sessions
blocked 5-18s in the all_sessions.state_db_overrides stage:
_apply_sidebar_state_db_overrides read state.db for every row (2400+ ids)
on every concurrent poll. With the gateway-watcher reading and the agent
writing the same state.db, this became lock/IO contention that piled up
concurrent polls and flapped the UI to 'Connection lost'.

Cap the override lookup to the top-N most-recent (paint-priority) rows
that are actually visible, mirroring the existing lineage-enrichment cap
(#4638). The caller passes an already pinned-first/newest-first sorted
list, so the top-N covers the visible window; rows beyond the cap keep
their JSON source/title and are corrected lazily on history-panel open.
Cap defaults to 300, env-configurable via
HERMES_WEBUI_STATE_DB_OVERRIDE_TOP_N (0 disables). Fails open.

Reported by @latipun, reproduced by @b3nw.

Co-authored-by: latipun <latipun@users.noreply.github.com>
2026-06-29 00:40:10 +00:00
nesquena-hermesandsantastabber a8e921d13a fix(sidebar): scope hidden archived-parent references to active project + source
The sidebar_reference_sessions payload arrives project-unscoped from
/api/sessions. _renderSidebarRowsFromRawSessions appended it to EVERY
render unconditionally, so an archived parent in project B entered a
project-A-filtered render's suppression context and silently hid a
visible project-A child/fork (rendered as []). Add
_scopedSidebarReferenceRows() to filter references to the same project +
webui/cli source bucket as the render they feed, mirroring
_partitionSidebarSessionRows. Regression test locks the cross-project
case; the existing suppression test now exercises the scoped helper.

Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-29 00:21:04 +00:00
nesquena-hermes 863dc4e041 Splash logo: drop app-icon tile, free-breathing mark with per-theme glow
The new-conversation splash mark was wrapped in a glossy navy rounded-rect
that reads as a macOS app-launcher tile ('this is an app') rather than a
brand mark. Major AI chat UIs (Claude, ChatGPT, Gemini) all render their
empty-state mark bare. Remove the tile, scale the winged caduceus up
(~88px), and back it with a soft cyan/blue radial 'spirit' bloom.

Light and dark get separately tuned marks: dark keeps the bright
cyan->blue gradient + luminous bloom; light uses a deeper #0AA6CC->#1E63D6
gradient + cooler restrained bloom so the mark holds contrast on the pale
background instead of washing out. Entrance is gated behind
prefers-reduced-motion.
2026-06-28 23:42:02 +00:00
nesquena-hermes 4d79de266c Merge master into stage-5155 (catch up to v0.51.728) 2026-06-28 23:25:57 +00:00
nesquena-hermes 39fe1a50a5 docs(changelog): stamp #5155 extension browser-local settings surface (RFC #5094) 2026-06-28 23:21:48 +00:00
nesquena-hermes f968de8d35 merge #5155 extension settings surface 2026-06-28 23:07:00 +00:00
nesquena-hermes ffcb9b47b4 docs(changelog): stamp #5128 #5129 #5151 fix batch 2026-06-28 23:04:47 +00:00
nesquena-hermes 9736e943bb merge #5151 distinguish offline gateway approval warnings 2026-06-28 22:54:54 +00:00
nesquena-hermes 751ba20fec merge #5129 surface provider auth failures in chat 2026-06-28 22:54:54 +00:00
nesquena-hermes 1a4dcd4fb5 merge #5128 custom-provider bare-model wakeup repair 2026-06-28 22:54:54 +00:00
nesquena-hermes 80661f03ce docs(changelog): defer non-critical session-switch refreshes (#5117) + scope-undef gate: allowlist requestIdleCallback/cancelIdleCallback 2026-06-28 22:38:22 +00:00
nesquena-hermes fc02953ea6 Merge branch 'pr-5117-head' into stage/5117-defer 2026-06-28 22:25:26 +00:00
nesquena-hermesandrodboev 7b12dff3e6 test(#5121): make auth-failure tests host-independent
The 4 terminal-auth-failure tests assert an unrecoverable 401 surfaces an
apperror / persisted _error turn. The settlement path first attempts the
#1401 credential self-heal, which reads ~/.hermes/auth.json. On a host with
populated credentials, self-heal can succeed and retry the mock agent,
swallowing the error the test expects -- so the result depended on host
credentials (green on CI / Windows with no creds, red on a live agent box).

Add an autouse fixture that forces _attempt_credential_self_heal to a no-op
so every host exercises the unrecoverable-failure path. The one test that
verifies a successful retry already patches the symbol in its own body, so it
is unaffected.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-28 22:20:28 +00:00
nesquena-hermes beb883a159 docs(changelog): clear stale gateway approval mirrors on teardown (#5147) 2026-06-28 21:57:12 +00:00
nesquena-hermes c556262159 Merge branch 'pr-5147-head' into stage/5147-approval 2026-06-28 21:48:51 +00:00
nesquena-hermes 230a3af684 docs(changelog): pinned streaming scroll restoration (#5118) 2026-06-28 21:40:18 +00:00
nesquena-hermes fc087c067f Merge branch 'pr-5118-head' into stage/5118-scroll 2026-06-28 21:22:53 +00:00
nesquena-hermes 48e6df9e39 Merge branch 'pr-5119-head' into stage/5119-i18n 2026-06-28 21:10:54 +00:00
nesquena-hermes d482f8d256 docs(changelog): cron-snapshot profile recompute (#5131) 2026-06-28 21:10:28 +00:00
nesquena-hermes fb7b16e723 Merge branch 'pr-5131-head' into stage/5131-cron 2026-06-28 20:51:44 +00:00
nesquena-hermes 5c2d4f243b docs(changelog): new cyan caduceus brand mark (#5113) 2026-06-28 20:37:05 +00:00
nesquena-hermes a17080d61f Merge branch 'pr-5113-stage' into stage/5113-brand 2026-06-28 20:36:46 +00:00
nesquena-hermes 9b17c1c80a Merge branch 'pr-5090-head' into stage/p1-lowrisk-a 2026-06-28 20:22:22 +00:00
nesquena-hermes 822effe0d5 test(graphite-skin): drop stale .app-titlebar-icon rect assertion
PR #5113 (new cyan caduceus brand mark) intentionally removed the per-skin
.app-titlebar-icon rect/path/circle recolor overrides — the old gold mark was
forced to the skin accent, but the new brand badge is self-colored and constant
across themes. test_graphite_skin_tunes_workbench_chrome still asserted that
removed rule. Dropped the obsolete assertion; the rest of the graphite chrome
coverage is unchanged.
2026-06-28 20:11:22 +00:00
nesquena-hermes 1cbf364f48 Merge remote-tracking branch 'origin/master' into pr5113 2026-06-28 20:05:50 +00:00
nesquena-hermes e60689bf28 docs(changelog): brick batch entries (#5122 SSE blank-out, #5096 fork/edit/rewind context bundle #5124/#5125/#5126) 2026-06-28 19:37:26 +00:00
nesquena-hermesandclaw-io 2d249089fc fix(session): guard _evict_session_agent against closing a live session's _session_db
Mirror the worker's LRU-eviction ACTIVE_RUNS liveness check in
_evict_session_agent so truncate/clear/model-switch can't close the SessionDB
out from under an in-flight turn on the same session (#5096 Bug D follow-up).
The cache handle still drops (harmless — worker holds a local ref); only the
lifecycle commit + _session_db.close() are skipped while a run is active.

Co-authored-by: claw-io <claw-io@users.noreply.github.com>
2026-06-28 19:36:59 +00:00
nesquena-hermes 2ebcaeac64 Merge branch 'pr-5122-head' into stage/brick-5096 2026-06-28 18:26:34 +00:00
nesquena-hermes aae7cc58d2 Merge branch 'pr-5126-head' into stage/brick-5096
# Conflicts:
#	api/session_ops.py
2026-06-28 18:14:03 +00:00
nesquena-hermes 57061d0b19 Merge branch 'pr-5125-head' into stage/brick-5096 2026-06-28 18:13:29 +00:00
nesquena-hermes c62a23d0bf Merge branch 'pr-5124-head' into stage/brick-5096 2026-06-28 18:13:29 +00:00
nesquena-hermesandksj5131513-ops 7ec7b9c942 fix(mobile): PWA reflow guard + avoid redundant same-session refresh (salvage of #3976)
Salvages PR #3976 by ksj5131513-ops, ported fresh onto current master and
re-applied on top of #4195's external-refresh gate (the original PR predated
#3916/#4195's rework of refreshActiveSessionIfExternallyUpdated).

Two parts:

1. Avoid an unconditional same-session force reload the moment streaming
   settles. _scheduleActiveSessionIdleReload previously called
   loadSession(force:true) on every idle reconcile, which on mobile PWA caused
   a visible end-of-turn flash and could briefly restore the pane with stale
   layout geometry. It now reconciles against server metadata first via
   refreshActiveSessionIfExternallyUpdated('idle-reconcile', {ignoreStreamJustFinished:true})
   — reloading ONLY when the message count actually changed — and falls back to
   the original forced reload only if the metadata probe itself fails.

   This COEXISTS with #4195: the poll-only external-session early-return
   (if((reason||'poll')==='poll' && !_isExternalSession(S.session)) return) is
   left untouched. The idle path uses a non-'poll' reason so it never trips that
   gate, and opts (read via arguments[1], same pattern as loadSession) only
   bypasses the post-stream cooldown, not the external gate. The function keeps
   its (reason) signature and now returns a status string
   ('skipped'/'unchanged'/'reloaded'/'failed') for callers that want it.

2. Mobile viewport reflow guard for PWA visualViewport changes. When the
   on-screen keyboard / browser chrome shows or hides, the phone layout can stay
   painted against stale geometry. _forceMobileViewportReflow() toggles a
   one-frame html.viewport-reflow class (cheap GPU-promotion transform) to force
   a repaint, then resyncs the workspace panel + sidebar aria. Wired to
   visualViewport resize/scroll (debounced) where available, falling back to the
   window resize handler otherwise. Adds safe-area-inset padding to topbar,
   messages-inner, and composer-wrap on phones. Uses overflow-x:clip (NOT hidden)
   on .layout per the #4856 lesson to avoid coercing overflow-y into a scroller.

Co-authored-by: ksj5131513-ops <ksj5131513-ops@users.noreply.github.com>
2026-06-28 09:02:10 +00:00
nesquena-hermesandlidi1011 94d952c3bb fix: harden None providers config across onboarding/providers/routes (salvage of #3967)
A config.yaml with an explicit empty/null `providers:` key parses to None in
PyYAML. `cfg.get("providers", {})` only returns the {} default when the key is
*absent*, so an explicit null yields None instead of an empty mapping. master
already guards each read with `isinstance(providers_cfg, dict)`, but the value
fed to that guard is still a footgun: the natural chained form
`cfg.get("providers", {}).get(...)` (already fixed once in api/config.py)
crashes with AttributeError on None.

Harden every providers-key read in the three remaining files to
`cfg.get("providers") or {}` so an explicit null degrades to an empty dict at
the source, matching the existing api/config.py convention. Sites hardened:

  api/onboarding.py:578  (_provider_api_key_present)
  api/providers.py:1051  (_provider_has_shadowed_codex_oauth_value)
  api/providers.py:1219  (_provider_has_key)
  api/providers.py:1265  (_get_provider_api_key)
  api/providers.py:2348  (get_providers catalog)
  api/providers.py:2781  (_clean_provider_key_from_config)
  api/routes.py:5045     (_context_length_config_api_key_for_provider)
  api/routes.py:5110     (_context_length_lookup_inputs_for_model)
  api/routes.py:15916    (_handle_live_models)

Adds tests/test_none_providers_config_guard.py: per-file source-form pins (fail
on master / on any single-file revert) plus behavioural checks driving the real
functions with `providers: None` to prove no crash and parity with the
empty-mapping config. Updates the existing test_issue3717 routes-string pin to
match the hardened form.

This is a focused salvage of grab-bag PR #3967 -- only the None-providers
hardening is taken; the PR's unrelated changes (account-usage worker-pool
refactor, function removals) are intentionally dropped.

Co-authored-by: lidi1011 <lidi1011@users.noreply.github.com>
2026-06-28 08:57:39 +00:00
nesquena-hermesandgkd2323c af0835a083 fix(streaming): delegate image routing to agent canonical, preserve unknown-model multimodal (salvage of #4113)
WebUI's api/streaming.py:_resolve_image_input_mode was a LOCAL
re-implementation of agent/image_routing.py:decide_image_input_mode that
had DIVERGED from the canonical function. The local copy never consulted
the active model's vision capability — it only checked a handful of config
heuristics — so it returned "native" (embedding the image as an image_url
part) even for models KNOWN to lack vision. That silently sends pixels to a
blind model (#21160-class failure) instead of routing through the text
(vision_analyze) pipeline.

Fix: delegate to the canonical agent.image_routing.decide_image_input_mode
(single source of truth), with a try/except fallback that preserves the
historical WebUI behaviour when the agent package is not importable (e.g.
the WebUI standalone test environment).

Carve-out preserved: the canonical router conservatively returns "text" for
UNKNOWN/custom models (no models.dev capability data). WebUI historically
forwards those NATIVELY and relies on the agent's strip-and-retry guard
(_try_shrink_image_parts_in_messages / _strip_images_from_messages) to
downgrade on a provider rejection. We keep that: a canonical "text" verdict
is only honoured when there's a real signal — an explicit user choice
(image_input_mode: text or a configured auxiliary.vision backend) or a model
KNOWN to lack vision. Otherwise we forward native.

Tests:
- Existing gateway test (forwards_image_attachments_as_multimodal_parts)
  stays green — unknown "test-model" still forwards native.
- New non-vacuous regression: a model KNOWN to be text-only
  (supports_vision == False) now routes "text" — this FAILS against master,
  where the old local copy returned "native" for the same config.
- Added coverage for the unknown-model carve-out, known-vision native path,
  explicit-text-signal precedence, and the agent-unavailable fallback.

Salvage of #4113 by @gkd2323c. The original PR delegated fully to canonical
but did not add the unknown-model carve-out, which would have regressed the
WebUI native-forwarding behaviour the gateway test encodes.

Co-authored-by: gkd2323c <gkd2323c@users.noreply.github.com>
2026-06-28 08:48:06 +00:00
nesquena-hermesandTomBanksAU 04066b4b43 feat(insights): surface prompt-cache hit rate (salvage of #3912)
Surfaces prompt-cache hit rate on the Insights page so cache-efficiency
(a key cost-optimisation signal) is visible at a glance.

Backend (api/routes.py, _handle_insights):
- Aggregate cache_read_tokens per model and per day, plus a total.
- Graceful fallback for older agent state.db schemas lacking the
  cache_read_tokens column (COALESCE + OperationalError second query).
- New response fields: total_cache_read_tokens, total_cache_hit_percent,
  per-model cache_read_tokens + cache_hit_percent, per-day cache_read_tokens.

Frontend:
- Models table gains a Cache column (panels.js + style.css 6-col grid).
- Daily bar tooltip includes the bounded cache hit rate.
- New i18n keys insights_model_cache / insights_cache_hit in all 13 locales.

Correctness fix (maintainer-flagged on #3912): the original computed
hit-rate as cache_read / input_tokens (reads-over-misses), which EXCEEDS
100% on cache-heavy sessions. This uses the FULL prompt total as the
denominator -- cache_read / (input_tokens + cache_read) -- via the shared
api.usage.prompt_cache_hit_percent helper, so it is bounded 0-100% and
means "% of the prompt served from cache". Returns None (rendered as a
muted dash) when there are no cache reads.

Tests (tests/test_insights.py):
- Updated existing assertions for the new response shape.
- Added a non-vacuous <=100% boundary test: a cache-heavy case
  (cache_read=9900, input=100) where the OLD formula reports 9900% but the
  bounded denominator yields 99%. Verified the test FAILS under the old
  formula and PASSES under the fix.

Salvage note: PR #3912's diff was ~99 releases stale (mostly re-derived old
code, illusory +5405). This ports ONLY the real ~190-line feature fresh
onto current master.

Closes #3911

Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
2026-06-28 08:11:22 +00:00
nesquena-hermes dcf8643093 docs: add desktop icon decision preview images 2026-06-28 08:04:53 +00:00
nesquena-hermes e1091ba977 docs: add brand-icon preview screenshot for PR 2026-06-28 07:52:50 +00:00
nesquena-hermes 5eff106e14 Swap brand to new cyan caduceus: favicon family, titlebar + empty-state logo (theme-safe navy badge), remove per-skin titlebar fill overrides
New brand caduceus (vector mark, cyan #08EBF1->#3889FD) on a radial-depth navy
badge. Favicon family regenerated (svg/ico/png/apple-touch). Titlebar and
empty-state logos now use the self-contained navy badge so they render correctly
on every theme (light + dark) without washing out. Removed 5 per-skin CSS rules
that force-filled the titlebar icon to a flat accent color (they assumed the old
monochrome caduceus).
2026-06-28 07:44:16 +00:00
nesquena-hermesandmerodahero 31a01e8adf fix(routes): claim CLI/TUI/Desktop sessions on POST, not just GET (salvage of #4153)
CLI/TUI/Desktop sessions were *recreated* on every POST to /api/chat/start
instead of *claimed*, because the claim/synthesis logic only ran on the GET
stub path. GET /api/session already synthesized a read-only stub from
state.db, but POST /api/chat/start unconditionally 404'd on any session_id
without a WebUI JSON sidecar — and the frontend 404 handler then ran its
empty-state self-heal, stripping the URL and silently discarding the user's
typed message.

This closes the GET/POST asymmetry by routing both endpoints through a new
shared helper, _claim_or_synthesize_cli_session, that materialises a
WebUI-owned Session on first write (via synth.save()).

Preserved guards:
- Denylist of foreign-owned sources via _is_claimable_cli_source:
  {claude_code, cron, external_agent, gateway, messaging, unknown}; cli/tui/
  desktop fall through to CLAIMABLE.
- The not_claimable arm returns 403 (NOT 404) and never reaches synth.save()
  (ownership guard). 403-vs-404 matters: the frontend 404 handler runs the
  empty-state self-heal that strips the URL, so a legitimately-listed
  read-only session must surface a refusal instead of vanishing.
- created_at mapping from state.db is load-bearing (Session.save() doesn't
  touch created_at), so claimed sidecars no longer sort as "Jan 1 1970".
- The 500 path uses _sanitize_error so a raw OSError can't leak the sidecar
  filesystem path.
- The #2782 deleted-WebUI-session 404 self-heal contract is preserved via
  _session_index_marks_was_webui.
- The GET path's _session_visible_to_active_profile gate is preserved.

Polish over the original PR:
- Test docstring rationale corrected re: updated_at — Session.save() defaults
  touch_updated_at=True so the claimed sidecar's updated_at is stamped to
  "now" regardless; only created_at is preserved. Assertions unchanged.
- touch_updated_at=True default kept (a claimed session jumps to "just now",
  which is the desired UX); no test pins persisted updated_at to the state.db
  value.
- Added a path-leak assertion to the 500-sanitisation test: the sanitized
  string must not contain a '/' segment from the session-store root.

Salvage of #4153.

Co-authored-by: merodahero <merodahero@users.noreply.github.com>
2026-06-28 07:43:46 +00:00
nesquena-hermes 031b5b342a fix(extensions): define TTS registry at module scope + route auto-read through registered engines
Codex gate found 2 functional bugs:
1. registerHermesTtsEngine was inside the voice-mode IIFE, which returns early
   on browsers without SpeechRecognition/speechSynthesis — so the documented API
   didn't exist there. Moved the registry (registerHermesTtsEngine / _hermesTtsSynth
   / option helpers) to module scope so it runs unconditionally.
2. autoReadLastAssistant() ignored a selected registered extension engine (only
   handled elevenlabs/edge, fell through to browser TTS). Added the registered-engine
   branch mirroring speakMessage(), with graceful fallback when the saved engine
   is no longer registered.
2026-06-28 06:03:08 +00:00
nesquena-hermes cd71403be9 feat(extensions): TTS-engine registration capability (registerHermesTtsEngine)
Lets a trusted local extension contribute a TTS engine that appears in the
Settings -> TTS Engine dropdown and is used by BOTH playback paths (voice-mode
auto-read in boot.js _speakResponse + the per-message Listen button in ui.js
speakMessage). Mirrors window.registerHermesSkin.

  window.registerHermesTtsEngine({ id, label, synthesize(text, opts) })
    -> Promise<ArrayBuffer|Blob> of audio; core coerces + plays through the same
       <audio> lifecycle as the Edge engine (stop/rearm in voice mode).

Core owns selection, the dropdown <option> (label via textContent, never HTML),
and playback. Built-in ids (browser/edge/elevenlabs) are reserved; invalid
descriptors are rejected; empty/failed synth degrades gracefully. The settings
panel re-adds registered options on render (the markup hardcodes only built-ins).

Unblocks TTS-engine extensions (e.g. a local VOICEVOX engine). Docs in
docs/EXTENSIONS.md; CHANGELOG [Unreleased]. New regression test drives the real
registry via a Node harness (register/validation/reserved-guard/synth coercion).

Full suite: 10893 passed, 0 failed. Verified end-to-end in a live browser with a
companion VOICEVOX extension: engine registers, appears in the dropdown
(browser/edge/elevenlabs/voicevox), and the Listen button routes through the
registered synthesize (both VOICEVOX steps called).
2026-06-28 05:43:02 +00:00
nesquena-hermes 0c89b49cf8 chore: nudge PR head re-sync (no-op) 2026-06-28 05:22:27 +00:00
nesquena-hermes e4152c0398 fix(extensions): render skin picker label via textContent, not innerHTML (XSS hardening)
Codex flagged that _buildSkinPicker injected the extension-controlled skin
label/name via btn.innerHTML, so a malicious registerHermesSkin descriptor
(label='<img src=x onerror=...>') could execute when Settings -> Appearance
builds the picker. Rebuild the button with DOM nodes + textContent for the
label and set swatch backgrounds via element.style.background. Adds regression
tests asserting the innerHTML sink is gone.
2026-06-28 05:22:26 +00:00
nesquena-hermes dfe64313dc theme-registration: preserve pending ext skin across reload + expand accent token allowlist
Found via live browser testing of the first consumer (e-ink skin):
- An extension-registered skin selected by the user was lost on reload: both
  the pre-paint inline script in index.html AND the boot-time appearance sync
  normalized the persisted skin against the (extension-less) known-skins list
  and clobbered localStorage back to 'default' before the extension's
  registerHermesSkin() could run. Both sites now PRESERVE an unknown non-default
  persisted skin (a likely pending extension skin), so it survives the reload
  and the extension re-applies it on load. The server push is skipped for a
  still-pending ext skin.
- Expanded the token allowlist to the full accent family
  (--accent-hover/-text/-bg/-bg-strong/-rgb + --surface-subtle) so a skin can
  fully recolor accent-driven chrome (active rail, chips, profile labels), not
  just --accent. Value regex now also accepts a bare RGB triple for --accent-rgb.

Regression tests added for both preservation sites. Full suite: 10876 passed.
2026-06-28 05:22:26 +00:00
nesquena-hermes 6d8135df3a feat(extensions): theme-registration capability — registerHermesSkin()
Lets a trusted local extension contribute a custom skin to the NATIVE
Settings -> Appearance picker (instead of a parallel theme switcher).
window.registerHermesSkin(descriptor) validates + sanitizes the skin in
core, once, so every theme extension inherits the guard:
- token names restricted to a documented allowlist
- values sanitized against a strict color/dimension regex (rejects url(),
  expression(), semicolons, braces, and other CSS-injection vectors)
- reserved built-in skin keys cannot be overwritten
- descriptor with no valid tokens is refused
- idempotent re-register (updates in place) for a live theme editor

Injects a managed <style> for the :root[data-skin] tokens, appends to
_SKINS + _VALID_SKINS, refreshes the picker, and re-applies a persisted
selection that was waiting on a late-registered skin.

Unblocks theme-pack and theme-creator extensions. Documented in
docs/EXTENSIONS.md. Regression test drives the real registration +
sanitization logic via a Node harness (adversarial input).

Full suite: 10874 passed, 0 failed.
2026-06-28 05:22:26 +00:00
nesquena-hermes 58ba991c0d feat(extensions): opt-in CSP frame-src allowlist (HERMES_WEBUI_CSP_FRAME_EXTRA)
Adds a configurable frame-src directive so an extension can embed a
self-hosted web app in an iframe (e.g. an 'external app tab' pinning
Grafana / Vaultwarden / a dashboard). Mirrors the existing
HERMES_WEBUI_CSP_CONNECT_EXTRA knob: space-separated http(s) origins with
optional *.subdomain wildcard + port, validated, ignored if malformed
(directive-injection / paths / ws scheme / bad ports all rejected).

- Default frame-src is 'self' only, so existing same-origin iframes are
  unchanged; default-off when the env var is unset.
- frame-ancestors stays 'none' -- this only governs what THIS page may embed,
  never who may embed the WebUI.
- Threaded through both the enforced and report-only CSP builders + the
  server.py Handler wrapper, so both headers stay identical.

Docs in docs/EXTENSIONS.md. 8 new regression tests
(tests/test_csp_frame_src_extra.py) cover default, valid widening, enforced
parity, directive-injection rejection, path/ws/port rejection, and
connect/frame independence. Existing CSP test suite still green.

Note: full suite shows 2 pre-existing order-dependent flakes
(test_issue1574 spawn-context, test_issue3283 import-order) that pass in
isolation and are untouched by this CSP-only diff.
2026-06-28 04:08:04 +00:00
nesquena-hermesandrodboev f95f8c39bb Release YV (v0.51.692): bound boot /api/models 401 via shared boot budget (#5026, fixes #5021)
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 03:03:08 +00:00
nesquena-hermesandrodboev f8e0dcf057 Release YU (v0.51.691): fail-open auth-persistence diagnostics (#5023, fixes #5022)
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 02:11:38 +00:00
nesquena-hermesandrodboev 60f0d949bd Release YT (v0.51.690): bind default workspace on fresh blank boot (#4971, fixes #4877)
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 01:25:46 +00:00
nesquena-hermesandrodboev 8eee473a13 Release YS (v0.51.689): bounded post-upgrade active-profile 401 recovery (#5018, fixes #5001)
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-26 22:20:10 +00:00
nesquena-hermesandsr-dotcom f2c8ce30c1 Release YR (v0.51.688): README clarify in-process chat + HERMES_API_URL scope (#5015)
Co-authored-by: sr-dotcom <sr-dotcom@users.noreply.github.com>
2026-06-26 21:32:36 +00:00
nesquena-hermesandjkobject f4645925c6 Release YQ (v0.51.687): macOS/non-procfs system-health metrics via optional psutil (#4616)
Co-authored-by: jkobject <jkobject@users.noreply.github.com>
2026-06-26 21:07:46 +00:00
nesquena-hermes dcdc8b2e13 Release YP (v0.51.686): enhanced-table copy boundary-case test coverage (#5013) 2026-06-26 20:05:11 +00:00
nesquena-hermes d42147a73e test(#5013): cover table-element-anchored + cell-to-prose enhanced-table copy fallthrough
Adds the two remaining enhanced-table-copy boundary cases flagged as optional
coverage when #4994 (fixes #4945) shipped: a selection anchored on the <table>
node itself, and a selection that starts in a cell and ends in surrounding prose,
both must fall through to native copy. Test-only; the shipped guard already
handles both correctly (no production change needed).
2026-06-26 19:55:10 +00:00
nesquena-hermesandrodboev b56f1aaca6 Release YO (v0.51.685): extract session-list cache to its own module (#5004, #1907)
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-26 18:46:30 +00:00
nesquena-hermesandrodboev 2f43a4c522 Release YN (v0.51.684): full-table-only enhanced-table copy sanitizer (#4994, fixes #4945)
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-26 18:11:18 +00:00
nesquena-hermes 9ffb907951 Release YM (v0.51.683): pip-style agent-root discovery (#4998, fixes #4700) 2026-06-26 17:35:41 +00:00
nesquena-hermes cf1c15bdcc Release YL (v0.51.682): indexed compression-continuation lookup (#4991, fixes #4990) 2026-06-26 16:40:55 +00:00
nesquena-hermesandfranksong2702 f8c4d50950 harden(#4991): session_id max() tiebreaker + membership-completeness doc (Opus gate findings)
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-26 16:40:27 +00:00
nesquena-hermesandrodboev 287b84359e Release YK (v0.51.681): Anthropic OAuth-token env detection parity (#4995, fixes #4770)
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-26 16:12:46 +00:00
nesquena-hermes 903dabd29b fix(providers): strip Anthropic env values so whitespace-only OAuth tokens don't false-positive (#4995 gate finding) 2026-06-26 15:44:15 +00:00
nesquena-hermes 877cdac3c6 Release YJ (v0.51.680): explicit session-index rebuild thread ownership (#4993, #3894) 2026-06-26 15:25:13 +00:00
nesquena-hermes 83fec56124 Release YI (v0.51.679): parallelize fresh sidebar boot fetches (#4992, fixes #4759) 2026-06-26 15:07:49 +00:00
nesquena-hermes a89c303912 Release YH (v0.51.678): suppress post-render scroll artifact across all intent modalities (#4970) 2026-06-26 13:44:39 +00:00
nesquena-hermes 5c0cda025a Release YG (v0.51.677): render cron run output as literal pre/code (#4977) 2026-06-26 12:08:01 +00:00
nesquena-hermes 148d7b1e99 Release YF (v0.51.676): scope pending user-turn dedupe to current tail (#4826, fixes #4825) 2026-06-26 11:23:49 +00:00
nesquena-hermes fdfed7a0d5 Release YE (v0.51.675): bounded state.db tail reads for paginated session loads (#4920, fixes #4918 P1) 2026-06-26 10:54:13 +00:00
nesquena-hermes b80d1f1a40 Release YD (v0.51.674): process-global request-diagnostics watchdog (#4973) 2026-06-26 08:04:27 +00:00
nesquena-hermes 2f7c5f01ee fix(#4973): replace per-request diagnostics Timer with a process-global watchdog thread
A threading.Timer per RequestDiagnostics spawns one OS thread per request, held
alive until finish() cancels it. Under sustained /api/sessions poll load this
exhausts the per-process thread cap (RuntimeError: can't start new thread) and
the server stops accepting connections. Replace with a single lazy-init
watchdog daemon thread that scans pending diags every ~1s; public API
unchanged. Caps timeout-tracking at 1 thread/process regardless of request rate.
2026-06-26 07:58:13 +00:00
nesquena-hermes 4331999858 Release YC (v0.51.673): stale local approval card clears instead of dead-ending (#4955, #4948 local variant, #4771 follow-up) 2026-06-26 07:09:27 +00:00
nesquena-hermes a58e1c8343 Release YB (v0.51.672): pre-aggregate cron sidebar latest-message ordering for missing-index scans (#4962) 2026-06-26 03:17:11 +00:00
nesquena-hermes 1c7b0b1abe Release YA (v0.51.671): move CLI sidebar cache rebuild out of the lock (SWR+singleflight) (#4952, closes #4842) 2026-06-26 02:54:15 +00:00
nesquena-hermes 1c0a86f556 Release XZ (v0.51.670): chronological tool-call rows before final answer incl equal-ts (#4893) 2026-06-26 02:37:08 +00:00
nesquena-hermes 20727e142e test(#4893): close Opus coverage gaps — multi-tool equal-ts tie + tool-result block no-split 2026-06-26 02:25:07 +00:00
nesquena-hermes b22bb85a6a Release XY (v0.51.669): persist OpenAI priority service_tier in main-model settings (#4543, fixes #4536) 2026-06-26 02:09:20 +00:00
nesquena-hermes b2510efcb9 i18n(#4543): add service_tier keys to the vi locale (added to master after this PR; strict locale parity) 2026-06-26 01:51:59 +00:00
nesquena-hermes f6fc7fc4d1 Release XX (v0.51.668): WebUI<->Desktop session reconciliation (#4834, fixes #4833) 2026-06-26 00:47:00 +00:00
nesquena-hermes a0e7f558fd Release XW (v0.51.667): compaction-marker writeback + code-redaction fix (#4823, fixes #4821) 2026-06-26 00:23:19 +00:00
nesquena-hermes 6bac10c39d Release XV (v0.51.666): local approvals no longer 409 as gateway run unavailable (#4950, #4771 regression, closes #4948) 2026-06-25 22:36:50 +00:00
nesquena-hermes bac8108b38 fix(#4771): don't 409 local approvals as "gateway run unavailable"
#4771 added an explicit relay-failure 409 (code: gateway_run_unavailable)
to /api/approval/respond so a gateway approval whose run is gone keeps its
card actionable instead of silently failing. But the 409 was emitted
regardless of backend.

On the DEFAULT local in-process backend, every guarded command parks an
entry in tools.approval._gateway_queues (via _await_gateway_decision), and
the local streaming notify callback mirrors it into _pending tagged with
_GATEWAY_MIRROR_FLAG. There is no gateway run and no _STREAM_RUN_IDS entry
by design. _gateway_pending_approval_without_run_id() therefore returned
True for that purely local approval, so the handler 409'd
("active run unavailable") and refused to resolve an approval that resolves
perfectly well locally. Local-backend users hit this on every approval
click (reported by b3nw on Discord; independently filed as #4948 by
claw-io, which also notes the local-runtime trigger).

Fix: gate the 409 on the WebUI actually running the gateway chat backend
(webui_gateway_chat_enabled). Local approvals fall through to the local
resolution path; the legitimate #4771 gateway behaviour is unchanged.

Tests:
- New tests/test_issue4771_local_approval_regression.py drives the real
  _handle_approval_respond handler and asserts: local backend -> 200 +
  agent thread unblocked; gateway backend -> 409 preserved. These fail
  before the fix (409) and pass after.
- The two existing #4771 tests that asserted the 409 are updated to pin
  HERMES_WEBUI_CHAT_BACKEND=gateway at respond time, since the 409 is now
  gateway-deployment behaviour.

Reported-by: b3nw
Reported-by: claw-io (#4948)
2026-06-25 22:21:56 +00:00
nesquena-hermes 6114edc7cd Release XU (v0.51.665): freeze CLI/cron sidebar cache key during streaming (#4908, #4842) 2026-06-25 22:09:03 +00:00
nesquena-hermes 7791370d49 docs(#4908): correct cache-invalidation comment per Opus gate — TTL is the backstop for externally-driven changes
Opus SHIP-WITH-FIXES (no code change required): the comment/docstring overstated
that the session-list-change listener covers ALL structural mutations. In-app
mutations fire it, but externally-driven changes (scheduled cron completion,
external CLI writes) don't — for those the 30s streaming TTL is the load-bearing
backstop (≤30s bounded, self-healing lag). Comment-only accuracy fix; logic
unchanged (both gates: perf fix correct, freeze effective, staleness bounded).
2026-06-25 21:57:36 +00:00
nesquena-hermes 79008b9333 Release XT (v0.51.664): transparent stream keeps prose chronological for mixed turns (#4932, #3397) 2026-06-25 21:52:09 +00:00