Commit Graph
7738 Commits
Author SHA1 Message Date
Rod Boev ea3163e234 fix(#5932): harden board workspace modal proof 2026-08-08 15:44:56 -04:00
Rod Boev eb9298ba91 fix(#5932): omit unchanged board workdir on settings edits 2026-08-08 15:44:55 -04:00
Rod Boev 2b0614ce35 feat(#5932): add per-board default workdir 2026-08-08 15:44:55 -04:00
nesquena-hermesandnesquena-hermes 7a94e34a6d Release exp-v0.52.181: compact active recovery snapshot transport (#6858, @allenliang2022) (#6860)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-08 19:12:33 +00:00
allenliang2022andallenliang2022 bfff32baf5 perf(session): reduce active recovery snapshot payloads (#6858)
Project active run-journal recovery snapshots into a non-mutating compact transport form before returning them from /api/session, removing duplicate representations while preserving the live message timestamp. Thanks @allenliang2022.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-08-08 19:11:09 +00:00
7f27942393 Release exp-v0.52.180: don't drop async delegation completions while origin session busy (#6632 + #6662) (#6852)
* fix: preserve async completion delivery attempts while busy

* fix(async): keep legacy busy completions retryable

* fix(async): extend keep_legacy_retrying to resolved-target transient-failure exits; keep formatting failures bounded (combined #6632+#6662 gate findings)

* Release exp-v0.52.180: don't drop async delegation completions delivered while origin session busy (#6632, #6662)

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-08-08 00:14:21 -07:00
21a8160338 Release exp-v0.52.179: gateway watcher index-covered fingerprint + failure-state preservation (#6821) (#6850)
* perf: keep gateway watcher fingerprint index-covered

* [verified] fix: preserve watcher state on projection failure

* Release exp-v0.52.179: keep gateway watcher fingerprint index-covered + preserve state on projection failure (#6821)

---------

Co-authored-by: StefanPieter <stefan@noble-pro.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 23:18:33 -07:00
c1fc3f2c79 Release exp-v0.52.178: honor configured appearance defaults for no-settings-file users (#6834) (#6849)
* fix(settings): _SETTINGS_DEFAULTS theme/skin never reach a user with no settings file

`_read_raw_settings_file()` returns `{}` for a MISSING settings.json. `{}` is a
dict, so `load_settings()` always took its `isinstance(stored, dict)` arms:

    settings["theme"], settings["skin"] = _normalize_appearance(
        stored.get("theme") if isinstance(stored, dict) else settings.get("theme"),
        stored.get("skin") if isinstance(stored, dict) else settings.get("skin"),
    )

`stored.get("theme")` is then None, `_normalize_appearance(None, None)` falls
through to its unknown-theme branch, and the result is ("dark", "default").
`_SETTINGS_DEFAULTS["theme"]` and `["skin"]` are therefore unreachable for the
one case they exist to serve: a user who has no settings file yet.

This is invisible on the shipped defaults, because dark/default is exactly what
the fallback produces — the two paths agree, so nothing ever caught it. It only
surfaces once the defaults are changed, at which point the dict silently does
nothing: a deployment (or fork, or a future change to the shipped values) sets an
appearance and every new user still gets dark.

The fix falls back to the defaults instead of to None, gated on the PAIR of
keys. Per-field (`or settings.get(...)`) looks equivalent and is not: with a
stored legacy theme and no stored skin, `slate` normalises to ("dark", "slate"),
but per-field fallback injects the default skin and returns ("dark", "default"),
silently dropping the legacy migration. That is the same absent-vs-explicit
distinction the boot script draws in #6808 — this is its server-side half. #6808
stops the browser fabricating a choice; without this, the server still answers
with the fallback rather than the configured default.

Behaviour on stock defaults is provably unchanged: `_normalize_appearance(None,
None)` and `_normalize_appearance("dark", "default")` both return
("dark", "default"). A stored preference is untouched in every case.

13 tests. Both directions are pinned, and both were checked by injection:
reverting api/config.py fails exactly the two "defaults are honoured" cases and
nothing else; substituting the per-field variant fails 8, including four of the
five legacy migrations. Ran the full settings/appearance surface — 61 test files,
873 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Release exp-v0.52.178: honor configured appearance defaults for users with no settings file (#6834)

---------

Co-authored-by: Hermes Agent <hermes@aip.de>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 23:06:57 -07:00
80e3574eca Release exp-v0.52.177: scope aux-title test module stubs (#6661) (#6848)
* test(#6630): isolate auxiliary client fixtures

* test(#6630): route both aux-title suites through one scoped module stub

* test(#6630): drop dead import and record the patch.dict restore constraint

* Release exp-v0.52.177: scope auxiliary-title test module stubs, stop cross-file sys.modules leak (#6661)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 22:47:51 -07:00
5e5a41e79f Release exp-v0.52.176: start profile list-cache TTL after row build (#6846) (#6847)
* fix(profiles): start list cache TTL after row build

Slow row builds currently consume the cache TTL before the result is stored.
Timestamp the cache at completion so every successful build receives the full TTL.

Refs #5364

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna

* Release exp-v0.52.176: start profile list-cache TTL after row build (#6846)

---------

Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 22:40:22 -07:00
nesquena-hermesandnesquena-hermes fcef6ba1af Release exp-v0.52.175: identical-prompt retry settlement regression (#6631) (#6845)
* test: pin identical-prompt retry settlement

* Release exp-v0.52.175: two-direction regression for identical-prompt retry settlement (#6631)

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 20:05:49 -07:00
e09ccbaac9 Release exp-v0.52.174: regression coverage for empty tool_calls drop on mirror path (#6803) (#6844)
* test(streaming): cover empty tool_calls drop in _api_safe_message_positions (#6796)

* Release exp-v0.52.174: regression coverage for empty tool_calls drop on mirror path (#6803)

---------

Co-authored-by: Webtecnica Bot <bot@webtecnica.com.br>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 19:53:02 -07:00
898a01b766 Release exp-v0.52.173: don't mutate system prompt on workspace switch (#6683) (#6841)
* fix(cache): don't mutate system prompt on workspace switch (#6672)

* test(#6672): pin created_workspace freeze + compact round-trip + legacy fallback (closes coverage gap flagged in review)

* Release exp-v0.52.173: don't mutate system prompt on workspace switch (#6683)

---------

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 19:42:15 -07:00
a818be1ed4 Release exp-v0.52.172: preserve reader scroll intent during live renders (#6453) (#6838)
* fix: preserve reader scroll intent during live renders

* fix: enforce programmatic scroll guard freshness

* test: update programmatic scroll guard assertion

* test: update scroll safety valve assertion

* fix: reconcile scroll intent helper after base refresh

* fix: timestamp fallback history scroll writes

* test: cover stale older-message fallback scroll guard

* test: execute the real slow-render history fallback

* test: provide the history fallback DOM helper

* test: retain async declaration in history fallback harness

* test: remove trailing blank line from scroll intent regression

* test(#6414): update scroll-intent harness for window._autoScrollFollow + _messageScrollInputGeneration (master drift)

* Release exp-v0.52.172: preserve reader scroll intent during live renders (#6453)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Frank Song <xuefu.song@qq.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 18:37:31 -07:00
37547a7bd1 Release exp-v0.52.171: keep settled Worklog in place across stream settlement (#6454) (#6837)
* test: prove worklog continuity across stream settle

* fix: keep settled worklog in place on stream done

* fix: preserve settle follow ordering

* test: wire settled worklog frame proof

* fix: preserve settled worklog detail disclosure

* fix: preserve default-open settled worklogs

* fix: preserve blank-owner worklogs

* fix: keep render fallback self-contained

* ci: retrigger hosted checks

* style(worklog): grow collapsed Worklog rail tap target to 44px on touch devices (#6454 follow-up)

* Release exp-v0.52.171: keep settled Worklog in place across stream settlement (#6454)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-07 18:06:57 -07:00
nesquena-hermes 9d142e7868 Merge pull request #6835 from nesquena/stage/6723-provider-resolver
Release exp-v0.52.170: resolve @provider:model through shared parser at gateway boundary (#6723)
2026-08-07 16:16:24 -07:00
nesquena-hermes b337609b38 Release exp-v0.52.170: resolve @provider:model through shared parser at gateway boundary (#6723) 2026-08-07 23:09:55 +00:00
dss539 41df2a7f83 docs(test): clarify qualified model routing boundary 2026-08-07 23:09:32 +00:00
dss539 707fef9f4f fix(session): strip @provider qualifier only at the gateway boundary
The previous revision stripped the '@provider:' qualifier from model_value
in _session_model_state_from_request BEFORE _resolve_compatible_session_model_state
ran. That defeated two behaviors that rely on the qualified form:

  1. provider-switch repair - a session pinned to a removed/unconfigured
     provider (@removed:mistral-large) no longer reverted to the active
     default; it returned (bare_model, removed) and targeted an unavailable
     provider.
  2. PR #6718's custom-provider normalizer, which needs the still-qualified
     value to map @custom:<slug>:<model> to the canonical slug.

Keep the qualifier through session-state resolution (matching master) and
strip it to the bare model ONLY at the gateway request boundary via
_gateway_model_field(), which already runs on both gateway request sites.

Red-before-green: the rewritten test asserts @removed:mistral-large repairs
to the active default; it failed on the prior head and passes now.
The gateway-body assertions (TestGatewayRequestBodiesCarryBareModel) still
prove only the outbound payload is bare.

Verified: 275 tests pass across the issue-6722, provider-mismatch,
model-resolution-fast-path, gateway-approval (legacy+runs), gateway-routing,
custom-provider, session-model-switch and model-not-found suites.
2026-08-07 23:09:32 +00:00
dss539 b296fa2ff7 test: cover @provider:model resolution for multi-segment providers
Pins the provider/model pair for every qualified-model shape at all
three layers that reach the gateway: the routes splitter, the provider
normalizer, the request-state builder, and both gateway request bodies
(legacy chat completions + runs API).

The table-driven cases include the shapes a positional colon split gets
wrong -- @custom:backup:model-a, @custom:backup:model-a:free,
@custom:<host>:<port>:model and @ollama-cloud:model:tag -- and assert
the routes splitter agrees with config._parse_provider_qualified_model_id()
exactly, so re-introducing a local split heuristic fails loudly.

The gateway assertions read the JSON actually written to the request
body rather than mocking the helper under test.

All 12 fail on master before the fix and pass after it.
2026-08-07 23:09:32 +00:00
dss539 eda1ddd6f3 fix(routes,gateway): resolve @provider:model through the shared parser
A provider-qualified model string (@provider:model) is deliberately kept
intact for internal routing (#1253), but three places on the way to the
Hermes gateway mishandled it:

  * _clean_session_model_provider returned the whole "provider:model"
    value as the provider, so the gateway saw a provider named
    "ollama-cloud:deepseek-v4-flash" -> "Unknown provider".
  * _session_model_state_from_request extracted the provider correctly
    but left the full "@provider:model" string as the model value.
  * both gateway request builders sent that string verbatim as
    body["model"], which the upstream provider API 404'd on.

Rather than add positional colon-split heuristics, route all three
through config._parse_provider_qualified_model_id() -- the existing
shared grammar that already distinguishes a multi-segment custom
provider ID (custom:<slug>, custom:<host>:<port>) from a model segment
carrying its own colon tag (:free, :0731). A positional split cannot
tell those two shapes apart, so any single split direction breaks the
other; the shared parser is the only thing that gets both right.

_split_provider_qualified_model now delegates to that parser too, which
fixes the same class of truncation at all of its call sites in routes.py
instead of only the two reported ones, and guarantees the provider used
for the routing decision matches the model sent in the request.

Fixes #6722
2026-08-07 23:09:32 +00:00
nesquena-hermes c31d607a69 Merge pull request #6833 from nesquena/stage/6774-dict-models
Release exp-v0.52.169: expand dict-shaped custom-provider models in Providers card (#6774)
2026-08-07 15:53:23 -07:00
nesquena-hermes 434c61a376 lint: drop unused pytest import in test_custom_provider_dict_models (F401) 2026-08-07 22:41:05 +00:00
nesquena-hermes 207ce37eed Release exp-v0.52.169: expand dict-shaped custom-provider models in Providers card (#6774) 2026-08-07 22:36:48 +00:00
Kim Pedersen 0f7f3b6740 fix: match picker's sticky-before-plural ordering for provider card
The model picker (api/config.py:7308-7314) places the singular ``model``
field first, then appends unique IDs from the ``models`` catalog with
dedup. The Providers card was doing the opposite — dropping the singular
model entirely when a non-empty catalog existed.

This caused a silent disagreement: the picker showed the sticky default
model first, while the Providers card omitted it. Verified empirically
with ``model: sticky-default, models: {Best, Fast}`` — picker returned
the sticky model, card did not.

Now the card uses the same sticky-before-plural ordering as the picker:
singular model first (stripped), then _configured_model_ids appended
with dedup.

Updated tests:
- test_singular_model_appears_first_before_dict_models (was
  test_dict_models_ignore_legacy_model_field): now asserts sticky-first
  ordering instead of exclusion
- test_whitespace_and_duplicate_keys_normalized: updated for sticky-first
  prepend
2026-08-07 22:36:19 +00:00
Kim Pedersen 7b91484d79 refactor: reuse _configured_model_ids for custom provider catalog
Replace the manual if/elif/elif model-collection chain with a call to
the shared _configured_model_ids normalizer from api/config.py. This
ensures the Providers card applies the same whitespace stripping, empty-
ID dropping, and de-duplication as the model picker — closing a gap
where keys like "Best", " Best ", and "   " would produce duplicate
or empty-looking pills on the card while the picker reported a clean
catalog.

The refactored fallback also guards against whitespace-only singular
model fields, preventing empty pills from the fallback path.

Added two regression tests:
- whitespace/duplicate key normalization
- whitespace-only model field produces no entries
2026-08-07 22:36:19 +00:00
Kim Pedersen fd7666d5eb fix: empty dict/list falls through to model field for provider card
Greptile identified a real inconsistency (P1): when models is an empty
dict and a singular model field exists, the Providers card showed zero
models while the model picker (get_available_models) still surfaced the
configured default model via its separate default_model path.

Fix: gate both the list and dict branches on truthiness so an empty
container falls through to the singular model fallback — matching the
picker's behaviour and ensuring the card always shows at least the
configured default model.

Updated test_empty_dict_falls_through_to_model_field accordingly.
2026-08-07 22:36:19 +00:00
Kim Pedersen c7caadf2ba test: rename misleading test, add dedup + ordering regression
Address nesquena-hermes review feedback:

- Rename test_dict_models_include_default_model_field →
  test_dict_models_ignore_legacy_model_field. The old name/docstring
  claimed the singular model field appears in the list, but the
  assertion required it to be absent. Now the contract is explicit:
  when models is a dict, the legacy model field is NOT appended.

- Add test_dict_models_no_duplicate_when_model_is_also_key: the old
  fixture used a model value not present in the dict, proving only
  that the field is ignored — not that duplicates are prevented when
  it matches a dict key. The new test sets model=Best with Best also
  as a dict key, and asserts exactly one entry with stable ordering.

- Strengthen assertions to check list equality (insertion order)
  rather than just set membership, since the provider card output
  is user-visible.
2026-08-07 22:36:19 +00:00
Kim Pedersen 2801744fca test: fix misleading empty-dict test name and docstring
Address Greptile review feedback: the test name said "falls through to
model field" but the assertion requires an empty list (no fallback).
Renamed to accurately describe the behavior — an empty dict yields
zero entries, and the model field is NOT used as a fallback.
2026-08-07 22:36:19 +00:00
Kim Pedersen 8d936a86a3 fix: expand dict-shaped models in custom_providers for get_providers()
The `models` field in a custom_providers config entry can be a list or a
dict. The dict shape is the standard format produced by `hermes config set`:

    custom_providers:
      - name: litellm
        model: Coding
        models:
          Best: {context_length: 128000}
          Coding: {context_length: 1000000}
          Fast: {}

get_providers() only handled the list shape (#6127), so the Providers
settings card showed just the single `model` field instead of the full
catalog. The model picker (get_available_models in api/config.py) already
handled both shapes via _configured_model_ids — only the providers card
was broken.

Add a dict branch alongside the existing list branch, with regression
tests covering dict, list, default-model interaction, and empty-dict
edge cases.
2026-08-07 22:36:19 +00:00
nesquena-hermes 218d586432 Merge pull request #6832 from nesquena/stage/6816-matrix-msg
Release exp-v0.52.168: classify Matrix gateway sessions as messaging (#6816)
2026-08-07 15:04:23 -07:00
nesquena-hermes 19b85e84a1 Release exp-v0.52.168: classify Matrix gateway sessions as messaging (#6816) 2026-08-07 21:55:07 +00:00
rudidev08 6c8d130a7b fix: classify Matrix gateway sessions as messaging 2026-08-07 21:54:52 +00:00
nesquena-hermes a4bd54ef71 Merge pull request #6831 from nesquena/stage/6823-fd-leak
Release exp-v0.52.167: close leaked state.db read-only connections (#6823)
2026-08-07 14:40:27 -07:00
nesquena-hermes afc3066ec0 Release exp-v0.52.167: close leaked state.db read-only connections (#6823) 2026-08-07 21:34:53 +00:00
Randy Marsden dcf881f1e8 fix: close leaked state.db read-only connections in session discoverability/recovery
Three sites used `with sqlite3.connect(f"file:...?mode=ro", uri=True) as conn:`,
which only scopes the transaction — sqlite3.Connection's context manager never
calls close(). Same recurring bug class as #1421/#1494/#2233/#3904. On a
long-lived server each poll leaked a state.db (+ -wal/-shm) fd until the
process hit macOS's 256 soft FD limit and writes started failing.

Wrap all three in contextlib.closing(), matching the pattern already used
elsewhere in these files.
2026-08-07 21:34:34 +00:00
nesquena-hermes 5f3117f81e Merge pull request #6788 from nesquena/stage/6498b-0806
Release exp-v0.52.166: honor memory_enabled/user_profile_enabled in memory panel (#6498)
2026-08-05 19:45:54 -07:00
nesquena-hermes d018233e84 lint: remove unused pathlib import in #6498 test (ruff F401) 2026-08-06 02:41:27 +00:00
nesquena-hermes e6e9dde276 changelog: #6498 gate memory/user-profile reads via nested config (re-gate) 2026-08-06 02:26:59 +00:00
nesquena-hermes 2c9e621b3d Merge PR #6498: gate memory/user-profile reads via nested config (re-gate) 2026-08-06 02:26:24 +00:00
nesquena-hermes 7fe2274ad0 Merge pull request #6787 from nesquena/stage/6486-0805b
Release exp-v0.52.165: show final assistant report with tool-limit status card (#6486)
2026-08-05 18:24:48 -07:00
webtecnica a5b1898e20 fix(api): read memory flags from nested memory config section (#6498) 2026-08-05 22:23:55 -03:00
nesquena-hermes 2673fd24e2 changelog: #6486 render final report alongside tool-limit card 2026-08-06 01:06:13 +00:00
nesquena-hermes 874d62bc3a Merge PR #6486: show final assistant report with tool-limit status card 2026-08-06 01:05:50 +00:00
nesquena-hermes 2d819d4d7f Merge pull request #6786 from nesquena/stage/6670-0805
Release exp-v0.52.164: active-turn user-row dedup on mid-run reload (#6670)
2026-08-05 17:53:43 -07:00
nesquena-hermes 84ace41206 changelog: #6670 don't duplicate active turn user row on mid-run reload 2026-08-06 00:27:33 +00:00
nesquena-hermes 8ecf391340 Merge PR #6670: don't duplicate active turn's user row on mid-run reload 2026-08-06 00:26:35 +00:00
nesquena-hermes 3d6c17aba6 Merge pull request #6785 from nesquena/stage/6688-0805
Release exp-v0.52.163: Force-recovery for dirty Stable no-ref checkouts (#6688)
2026-08-05 16:38:42 -07:00
nesquena-hermes 408d77a073 changelog: #6688 Force-recovery dirty stable no-ref checkout 2026-08-05 23:21:20 +00:00
nesquena-hermes 4c042dbfdf Merge PR #6688: clean dirty stable no-ref checkouts on Force recovery 2026-08-05 23:20:27 +00:00