Commit Graph
2028 Commits
Author SHA1 Message Date
santastabber 178db13512 fix: authorize session html media artifacts 2026-06-29 05:17:33 +00:00
Rod Boev a052f390f8 fix(updates): resolve git under macOS launchd (#5175) 2026-06-29 04:22:51 +00:00
Rod Boev 9d3f26a718 refactor(updates): drop unreachable agent restart fallback branch (#5156) 2026-06-29 03:44:06 +00:00
Rod Boev 31a443ff88 fix(health): preserve restart failure contract and proofs (#5156) 2026-06-29 03:44:06 +00:00
Rod Boev 6d80dd41d6 fix(updates): restart gateway after in-app agent update (#5156) 2026-06-29 03:44:06 +00:00
claw-ioandnesquena-hermes 159298d319 fix(profiles): use profile-scoped resolver for /api/profile/active default_workspace (Codex gate #5169)
Codex flagged that get_last_workspace() falls back to the GLOBAL last_workspace.txt
before the profile config.yaml — so a named profile without its own
last_workspace.txt would leak the global path, re-introducing the #5169 bug.
Add get_profile_default_workspace() (profile last_workspace.txt -> config.yaml ->
terminal.cwd -> default, NO global fallback) and call it from /api/profile/active.
Adds a regression test: global last_workspace.txt present + named profile with
only config.yaml workspace must return the profile path, never the global.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-29 01:45:17 +00:00
claw-ioandnesquena-hermes ec0ef4a0d3 fix(profiles): show active profile workspace on blank new-chat (#5169)
Expose a profile-scoped default_workspace on GET /api/profile/active (resolved
by the profile-aware get_last_workspace(), fail-open) and hydrate boot.js from
it, overriding the global GET /api/settings value. GET /api/settings stays
global-only so the settings UI can't clobber the shared default. Combines
claw-io's #5173 (primary) with the fail-open backend hardening + regression test
from #5171.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-29 01:25:58 +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
santastabber 5ed0ba6ef6 fix(sidebar): keep hidden archived parents as child references 2026-06-29 00:13:32 +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 f968de8d35 merge #5155 extension settings surface 2026-06-28 23:07:00 +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
Rod Boev 234fefe6ee fix(#5139): keep timeout capability probes on the unsupported path 2026-06-28 18:29:37 -04:00
Rod Boev aef8e524fd fix(extensions): keep settings-only installs canonical (#5094) 2026-06-28 18:25:47 -04:00
Rod Boev 23b7a7935a fix(#5139): keep older gateways on the unsupported warning path 2026-06-28 18:18:47 -04:00
Rod Boev 938f721b16 fix(extensions): reload before editing untrusted installs (#5094) 2026-06-28 18:07:52 -04:00
Rod Boev 615b023eb0 fix(extensions): pin schemas to trusted bootstrap state (#5094) 2026-06-28 17:59:36 -04:00
Rod Boev 343a8b50bf fix(#5139): distinguish offline gateway approval warnings 2026-06-28 17:53:37 -04:00
b3nwandb3nw 7a04b7e9aa fix: gate bare-model repair on profile provider match (#5128 review)
When session model_provider differs from profile model.provider, do not
expose profile_default_model for suffix repair. Fast path also requires
profile_provider to match requested_provider when profile_provider is set.

Addresses cross-provider mis-resolution (custom:other-proxy + bare suffix).

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 21:52:06 +00:00
Rod Boev cb96a5cf39 fix(extensions): trust only bootstrapped extension ids (#5094) 2026-06-28 17:51:42 -04:00
Rod Boev 07ed67a793 fix(extensions): keep runtime settings gated on owned storage (#5094) 2026-06-28 17:44:02 -04:00
Rod Boev 62be5b02ea fix(#5135): clear stale gateway approval mirrors on stream teardown 2026-06-28 17:32:55 -04:00
Rod Boev a76c794d6d feat(extensions): add browser-local extension settings surface (#5094) 2026-06-28 17:28:20 -04:00
nesquena-hermes fb7b16e723 Merge branch 'pr-5131-head' into stage/5131-cron 2026-06-28 20:51:44 +00:00
Rod Boev 2eeb0f0f53 fix(#5130): serialize profile-scoped snapshot repair 2026-06-28 16:03:25 -04:00
Rod Boev fd139132b6 fix(#5130): harden cron snapshot repair follow-up 2026-06-28 15:56:50 -04: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
Rod Boev 7e806d7194 fix(#5130): recompute cron snapshots in selected profile 2026-06-28 15:29:40 -04:00
Rod Boev 4763c2a9a4 Keep null error fields from tripping replay failure 2026-06-28 15:22:33 -04:00
Rod Boev 8fbc3dcc9f Preserve canonical provider error probes in auth terminal turns 2026-06-28 15:11:01 -04:00
b3nwandb3nw 159d8a96f4 fix: repair bare custom-provider model on process-wakeup fast path
Refs #5127.

When _resolve_compatible_session_model_state takes the #1855 fast path with
a bare runtime model suffix and a slash-qualified profile default, repair to
the full qualified ID for custom/custom:* providers.

Also load profile model.default when the session already has model_provider
so wakeup turns can apply that repair.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 18:34:10 +00:00
Rod Boev 3e27a8bafe fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 14:23:08 -04: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
b3nwandb3nw 6e499eb1dd fix(session): semantic context truncate on rewind + evict agent
Refs nesquena/hermes-webui#5096

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 18:01:56 +00:00
Rod Boev 3fafa3efc6 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 14:00:40 -04:00
b3nwandb3nw aacf860ccc fix(session): truncate context_messages when branching from fork point
Refs nesquena/hermes-webui#5096

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 17:59:46 +00:00
Rod Boev eb886836f5 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 13:37:18 -04:00
Rod Boev 5f02189939 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 13:18:32 -04:00
Rod Boev 6681bef796 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 13:04:27 -04: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-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
Stacey2911 b39c147b31 fix(workspace): live-sync active Workspace Todos tab 2026-06-28 07:28:30 +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 dbfa33a999 feat(moa): route /moa through WebUI via server-resolved MoA config (#5070, #5057)
Adds a dedicated GET /api/commands/moa/resolve endpoint + resolve_moa_config()
that reads the MoA preset/usage server-side. The /moa send-path passes only a
moa_config:true boolean to /api/chat/start; the server re-resolves the real
config itself (never trusts a browser-supplied dict) and threads it per-turn into
the live agent run. Fails closed (409) on gateway-backed sessions.

Replaces the originally-proposed broad 43-command subprocess approach (bounced)
with this narrow, security-reviewed /moa route. Maintainer fixes on top of the
contributor's converged design: moa_config is added to run_conversation only
when not None (older-agent TypeError safety) at the main call AND both auth
self-heal retry sites, and forwarded through the legacy-journal runtime adapter.

Gate: Codex SAFE (after the maintainer fixes) + full suite 10784 pass + the
activity-stream regression gate clean.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-28 00:23:51 +00:00
nesquena-hermesandfranksong2702 e2ade27bfe fix(anchor): preserve settled mixed content order (#4958)
When a settled assistant turn carries _anchor_activity_scene and has mixed
content[] (text interleaved with tool_use), promote that ordering into anchor
scene rows (prose / tool / thinking rows in sequence) instead of only the raw
content[] fallback, and mirror the same scene model in the backend hydration
path so a cold reload reconstructs an identical transcript.

Also fixes: post-tool_use text/thinking in a NON-FINAL assistant message was
silently dropped — only the turn-final assistant's post-last-tool text is the
"final answer" tail; earlier assistant messages keep post-tool content as
activity rows. Tail thinking is excluded from the final-answer text but still
emitted as a thinking row (verified on both JS and Python paths).

Tool-row de-duplication is conservative — ID-equal always merges; name/
invocation matches are cardinality-gated and same-message; body-prefix is only
a secondary confirmation inside the id-flexible branch (also requires matching
started_at + compatible name + invocation). It biases toward an extra visible
tool card over ever silently merging two distinct calls.

Gate: Codex SAFE (tail-thinking edge verified already-handled both sides),
Opus SHIP (dedup fails toward duplicate-not-loss; non-final preservation, XSS,
non-mixed regression all clean), full suite 10778 passed (only the 4 known
pre-existing flakes). One non-blocking follow-up noted: bring the prose/thinking
peer-dedup into JS↔Python parity (cosmetic cold-reload extra-narration row).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-27 22:54:29 +00:00