Commit Graph
100 Commits
Author SHA1 Message Date
nesquena-hermes a8de4e7c0a Merge pull request #1799 from nesquena/stage-312
v0.51.18 — 5-PR batch (#1783, #1789, #1790, #1791, #1794)
2026-05-06 23:43:40 -07:00
nesquena-hermes 9875967528 Merge pull request #1788 from nesquena/stage-311
v0.51.17 — 2-PR batch (#1780, #1782)
2026-05-06 21:54:15 -07:00
nesquena-hermes 697a7a10d1 Merge pull request #1781 from nesquena/stage-310
v0.51.16 — 3-PR batch (#1768, #1778, #1779)
2026-05-06 20:12:44 -07:00
nesquena-hermes 9cc106272f Merge pull request #1777 from nesquena/stage-309
v0.51.15 — 4-PR batch (#1762, #1767, #1769, #1770)
2026-05-06 19:06:58 -07:00
nesquena-hermes 2106083e71 Merge pull request #1763 from nesquena/stage-308
v0.51.14 — 4-PR contributor batch (#1756, #1757, #1760, #1761)
2026-05-06 15:22:13 -07:00
nesquena-hermes 704f8ab16a Merge pull request #1759 from nesquena/stage-307
v0.51.13 — Single-PR composer UX (#1758)
2026-05-06 13:15:59 -07:00
nesquena-hermes 34f2243899 Merge pull request #1755 from nesquena/stage-306
v0.51.12 — 3-PR batch (cron subprocess return + custom provider routing + session runtime invariants)
2026-05-06 11:25:46 -07:00
nesquena-hermes 9900248c2f Merge pull request #1751 from nesquena/stage-305
v0.51.11 — 3-PR batch (model picker race, theme-color meta, quote-strip)
2026-05-06 11:04:41 -07:00
nesquena-hermes 4edcb682fc Merge pull request #1745 from nesquena/stage-304
v0.51.10 — 2-PR batch (cron profile isolation + profile switch during streams)
2026-05-06 09:28:59 -07:00
nesquena-hermes e75d3b1836 Merge pull request #1740 from nesquena/stage-303
v0.51.9 — 2-PR batch (boot path + Codex session repair)
2026-05-06 08:21:42 -07:00
nesquena-hermes 85d0279fbb Merge pull request #1737 from nesquena/stage-302
v0.51.8 — 7-PR batch (Activity row, OAuth, scroll, profile context, CLI catalogs, sidebar hover)
2026-05-06 01:29:59 -07:00
nesquena-hermes d8cd5567e0 Merge pull request #1723 from nesquena/docs/1695-aiagent-troubleshooting
docs(troubleshooting): bake the #1695 diagnostic flow into the error message + a new troubleshooting doc
2026-05-05 15:16:08 -07:00
nesquena-hermes 29878259ca docs(troubleshooting): bake the #1695 diagnostic flow into the error message + a new troubleshooting doc
Closes #1695.

@Patrick-81 reported the bare "AIAgent not available -- check that
hermes-agent is on sys.path" error on a symlinked install (~/Programmes/hermes-agent
linked to ~/hermes-agent). The maintainer's response — three diagnostic
commands plus `pip install -e .` in the agent dir — fixed it for them.
This PR captures both halves of that learning so the next user with the
same shape doesn't have to file a new issue:

1. **Error message diagnostic block.** New helper
   `_aiagent_import_error_detail()` in api/streaming.py builds a multi-line
   diagnostic when the import fails, including:
     - the running Python interpreter
     - HERMES_WEBUI_AGENT_DIR (set value, or "(not set)")
     - sys.path entries that mention hermes/agent (or "no entries mention..."
       — itself a strong diagnostic signal)
     - the most-common fix (`pip install -e .` in the agent dir)
     - a pointer to docs/troubleshooting.md

   The original error message string is preserved as the FIRST line so
   existing log scrapers and docs-search keep matching.

   Helper is kept as a separate function so it stays out of the hot path
   until we actually need to raise — building it on every successful import
   would be wasted work.

2. **New docs/troubleshooting.md.** Symptom → Why → Diagnostic commands →
   Fix → When-to-file-a-bug template, with one entry to start: the
   "AIAgent not available" flow Patrick-81 walked through. Future
   recurring failure modes follow the same template. Required a one-line
   addition to .gitignore — docs/* is gitignored with an allowlist, and
   the new file needed `!docs/troubleshooting.md` to be tracked.

3. **README link.** docs/troubleshooting.md added to the `## Docs` section
   so users know where to look first.

13 regression tests in tests/test_1695_aiagent_import_error_detail.py:
9 for the helper output shape (preserves original message line, includes
running python, shows HERMES_WEBUI_AGENT_DIR set/unset both ways, includes
pip-install-e hint, points at troubleshooting doc, lists relevant sys.path
entries when present, says "no entries..." when absent, output is multi-line)
plus 4 for the docs-presence regression (file exists, has the AIAgent
section, includes pip install -e ., describes the diagnostic chain with
readlink + agent/__init__.py verification).

190 streaming/aiagent tests pass after the change. ast.parse on
api/streaming.py clean.

CI failure on prior push was due to the docs/* gitignore swallowing the
new troubleshooting.md file silently — this commit adds the allowlist
entry so the file is tracked.
2026-05-05 22:14:07 +00:00
nesquena-hermes a6e2bbb263 Merge pull request #1724 from nesquena/stage-303
v0.51.6 — 5-PR full-sweep batch
2026-05-05 15:11:06 -07:00
nesquena-hermes 0ea3dfbdd1 Merge pull request #1713 from nesquena/stage-302
v0.51.5 — 4-PR full-sweep batch
2026-05-05 11:00:37 -07:00
nesquena-hermes cebca4700b Merge pull request #1708 from nesquena/fix/1707-workspace-name-click
fix(workspace): preserve single-click open + double-click rename on filename (#1707)
2026-05-05 09:16:42 -07:00
nesquena-hermes b5e8e67d71 fix(workspace): preserve single-click open + double-click rename on filename (#1707)
Closes #1707 — single-click on a workspace tree filename did nothing.

#1698 was a regression where the filename's dblclick rename handler was
unreachable because the row's el.onclick (openFile) fired synchronously
on the first click. The fix in #1702 stopped click propagation on nameEl
— but that broke single-click activation entirely (#1707): clicking the
filename now did nothing, you had to click the icon or row whitespace
to open the file.

Restored fix preserves both intents via a 300ms debounced delegator:

  let _nameClickTimer = null;
  nameEl.onclick = (e) => {
    e.stopPropagation();
    if (_nameClickTimer) { clearTimeout(_nameClickTimer); _nameClickTimer = null; }
    _nameClickTimer = setTimeout(() => {
      _nameClickTimer = null;
      if (typeof el.onclick === 'function') el.onclick(e);
    }, 300);
  };
  nameEl.ondblclick = (e) => {
    e.stopPropagation();
    if (_nameClickTimer) { clearTimeout(_nameClickTimer); _nameClickTimer = null; }
    // ... existing rename body
  };

Single-click on nameEl schedules a setTimeout that calls el.onclick(e)
after the dblclick threshold passes (300ms — matches the OS dblclick
threshold on most platforms). Double-click cancels the pending timer
and triggers the existing rename input.

Cost: 300ms latency on file-open clicks. Acceptable trade for keeping
rename reachable on single-click.

Also updated tests/test_workspace_tree_rename.py to accept both the
pre-#1707 (pure stopPropagation) and post-#1707 (debounced delegator)
shapes — the original assertion was too narrow and would have rejected
the correct fix.

9 new regression tests in tests/test_1707_workspace_filename_click.py:
  - 6 source-level static-analysis checks on the patched handler shape
  - 3 behavioral tests via Node VM (synthesize click → 300ms delay,
    click → dblclick within tick → assert rename mounts + openFile
    is not called).

7 of 9 tests fail on master pre-fix (verified); all 9 pass after.
2026-05-05 16:13:58 +00:00
nesquena-hermes 4daa23874a Merge pull request #1707 from nesquena/stage-301
v0.51.4 — 10-PR full-sweep batch
2026-05-05 08:56:38 -07:00
nesquena-hermes 1cde702d47 Merge pull request #1683 from nesquena/stage-300
v0.51.3 — 3-PR follow-up batch (#1671, #1673, #1676)
2026-05-04 19:43:16 -07:00
nesquena-hermes fcc83284e3 Merge pull request #1682 from nesquena/stage-299
v0.51.2 — 3-PR follow-up + sidebar scroll hotfix
2026-05-04 19:22:03 -07:00
nesquena-hermes e23ba59df2 Merge pull request #1681 from nesquena/stage-298
v0.51.1 — 11-PR contributor batch from @Michaelyklam
2026-05-04 18:42:28 -07:00
nesquena-hermes 2bbaad3135 Merge pull request #1675 from nesquena/feat/kanban-multiboard-and-sse
feat(kanban): multi-board management + SSE live event stream
2026-05-04 17:56:38 -07:00
nesquena-hermes 134433f8d9 Merge pull request #1661 from nesquena/stage-297
Release v0.50.297 — 3-PR batch (Docker regression fix + OAuth cancel race + persistent-host health hardening)
2026-05-04 15:52:51 -07:00
nesquena-hermes e6cf801ef4 Merge pull request #1652 from nesquena/stage-296
Release v0.50.296 — 3-PR batch (TPS in headers + session save mode + Codex OAuth onboarding)
2026-05-04 14:40:34 -07:00
nesquena-hermes 4085a1ff4d Merge pull request #1643 from nesquena/stage-295
Release v0.50.295 — 3-PR batch (YAML/JSON/diff newlines + macOS scroll race + custom:* providers + glued-bold-lift raw pre)
2026-05-04 11:39:49 -07:00
nesquena-hermes cbfc544f50 fix(renderer): YAML/JSON/diff code blocks lose newlines (#1618 / #1463)
Closes #1618 (reported by @Zixim) and corrects #1463's previous fix.

Bug: YAML, JSON, and diff/patch fenced code blocks render flattened to a
single line. Reporter noted the bug persisted v0.50.279 -> v0.50.291 ->
v0.50.292 despite PR #1516's CSS-only "fix".

Root cause: PR #484 (v0.50.237) added a JSON/YAML tree-viewer that routes
those languages through <div class="code-tree-wrap">...<pre class="tree-raw-view">
instead of bare <pre>. Same release added the diff/patch coloring path
that emits <pre class="diff-block">. The _pre_stash regex at
static/ui.js:1914 matched only literal <pre> with no attributes:

    <pre>[\s\S]*?<\/pre>

Both new shapes failed to match, fell through to the paragraph-wrap pass,
and \n characters inside the code blocks got replaced with <br> tags
inside <code>. By the time Prism ran, no newlines remained for the CSS
rule (PR #1516, language-yaml .token { white-space: pre !important }) to
preserve.

Fix: relax the regex to accept any attribute on <pre>:

    <pre>[\s\S]*?<\/pre>  ->  <pre[^>]*>[\s\S]*?<\/pre>

One regex character. Pulls JSON, YAML, and diff/patch blocks into the
stash so paragraph-wrap can't mangle them. Bash, Python, Go, etc. were
never affected because they emit bare <pre>.

Tests: 9 new (2 source-string invariants + 7 behavioural via node-driver
against the actual static/ui.js renderMd()). 6 of the 7 behavioural tests
fail on master and pass with the fix; the 3 sanity checks (yml-alias,
bash, mermaid) pass on both.

Plus widened source-scan window in 3 pre-existing test_745 assertions
from 400 to 1500 chars. The new comment block above the fixed regex
pushed it past the previous scan window. Pure window-narrowness bug,
not a behavior regression.

4245 -> 4254 passing.
2026-05-04 18:11:58 +00:00
nesquena-hermes 304a422814 Merge pull request #1638 from nesquena/stage-294
Release v0.50.294 — 3-PR batch (streaming stability trio + cache version stamp + race fix + readonly fs guard)
2026-05-04 10:27:00 -07:00
nesquena-hermes 66b925f59d fix(cache): stamp /api/models disk cache with WebUI version + schema version (#1633)
Closes #1633. STATE_DIR/models_cache.json was persisted across server
restarts without any version stamp, so a Docker container update from
version A to B read the cache file written by version A — users saw
stale picker contents (missing models, phantom provider groups) for
up to 24 hours until either the TTL expired, an unrelated provider
edit triggered invalidate_models_cache(), or they manually deleted
the file.

Reporter Deor (Discord) updated to v0.50.292 — which contained fixes
for #1538, #1539, and #1568 — did a hard refresh and cleared site
data, and still saw byte-for-byte identical picker contents because
the server kept reading the v0.50.281 cache file off the host-mounted
state volume.

Fix:
  * _save_models_cache_to_disk() stamps payloads with _webui_version
    (resolved lazily from api.updates.WEBUI_VERSION via sys.modules
    lookup to avoid the api.config <-> api.updates circular import)
    and _schema_version = 2.
  * New _is_loadable_disk_cache() validator checks both stamps in
    addition to shape. Mismatch on either field rejects the load.
  * _load_models_cache_from_disk() calls the new validator and
    strips the disk-only metadata before returning, so the rest of
    the code sees the same shape it always did.
  * _is_valid_models_cache() kept loose (shape-only) so in-memory
    cache writes that never touch disk don't fail validation.

Schema version is independent of the WebUI version stamp so future
cache-shape changes can invalidate older releases without relying
on a tag bump alone.

Early-init edge case (api.updates not yet loaded) skips the version
check rather than wedging the boot — at worst an unstamped file is
written once and rejected on the next call.

Updated existing tests/test_model_cache_metadata.py to use subset/
round-trip semantics rather than byte-for-byte equality, since the
disk payload now has additional stamps. The four response-shape
fields still round-trip verbatim; the load result is unchanged
(stamps stripped). 19 new regression tests.

4180 -> 4199 tests pass.
2026-05-04 17:03:02 +00:00
nesquena-hermes 040cb8af70 Apply Opus pre-release SHOULD-FIX + NITs (in-PR per release policy)
SHOULD-FIX: rate-limit _repair_stale_pending repair-firing telemetry. Switch
from unconditional logger.warning to age-keyed: WARNING when pending_age <
5min (the diagnostically valuable race window — actual leak-path candidates
that slipped past the grace guard) and DEBUG for the long-tail (orphaned
sidecars from prior process lifetimes). Prevents reconnect loops on stuck
sessions from flooding the log while preserving the diagnostic signal we
want for tuning _REPAIR_STALE_PENDING_GRACE_SECONDS empirically.

NIT: _LOCAL_SERVER_PROVIDERS expanded with lm-studio (hyphenated alias used
in some custom_providers configs and already recognized at api/config.py:2189
for SSRF host trust) and localai (LocalAI project). Test parametrize expanded
from 7 to 11 names, also covering pre-existing koboldcpp and textgen for
symmetry. +4 regression tests.

NIT (docs): CHANGELOG callout for the RFC1918 behavior change. Internal-
network OpenAI-compatible proxies now preserve the model prefix on private-IP
base_urls. Documented the migration path: configure as a custom_providers
entry to bypass the local-server detection.

NIT (deferred, optional): narrowing the heuristic to is_loopback only is
left as future work; the broader scope was an explicit goal in the bug
body and Opus flagged it as SHOULD-DISCUSS-but-not-block.

4184 -> 4188 passing. 0 regressions. ~10 LOC absorbed total.
2026-05-04 16:50:22 +00:00
nesquena-hermes bea57beba9 fix(streaming): SSE heartbeat alignment, repair grace period, local-server model id preservation (#1623, #1624, #1625)
Closes #1623 — Lower SSE app heartbeat from 30s to 5s at every long-lived
handler (main agent, terminal, gateway-watcher, approval-poller, clarify-poller).
Kernel TCP keepalive declares peer dead at 25s worst-case (10s KEEPIDLE +
5s KEEPINTVL * 3 KEEPCNT, added v0.50.289 #1581). 30s app heartbeat let the
kernel tear sockets down on flaky networks before the app sent its first
keepalive byte — drops at ~10s during long thinking phases. New named
constant _SSE_HEARTBEAT_INTERVAL_SECONDS=5; regression test pins the
inequality (app_heartbeat * 2 <= kernel_window) so future tuning can't
re-introduce the misalignment.

Closes #1624 — Add 30s grace period to _repair_stale_pending() trigger.
Without it, any narrow race between the streaming thread clearing
pending_user_message and STREAMS.pop(stream_id) produces a false-positive
'Previous turn did not complete.' marker on a turn that finished correctly
(reproducible after every command-approval turn). Defense-in-depth, not
the root-cause fix — the actual streaming-thread leak path is tracked
separately. Falsy pending_started_at (legacy sidecars) treated as
'old enough' so legitimate legacy-data recovery still works. Plus
logger.warning telemetry on every legitimate repair so the next batch of
user reports tells us whether the underlying race still fires.

Closes #1625 — Local model servers (LM Studio, Ollama, llama.cpp, vLLM,
TabbyAPI, koboldcpp, textgen-webui) now keep the full HuggingFace-style
model id (e.g. 'qwen/qwen3.6-27b' instead of stripped 'qwen3.6-27b'). New
_LOCAL_SERVER_PROVIDERS set + _base_url_points_at_local_server() loopback/
RFC1918 heuristic — either signal triggers no-strip. Backward compat
preserved for OpenAI-compatible proxies on public hosts (LiteLLM at
litellm.example.com still strips openai/gpt-5.4 -> gpt-5.4). Updated the
existing #230/#433 test to reflect that #1625 supersedes the strip-on-custom
rule for loopback hosts (see api/config.py and test_model_resolver.py
docstring update). Reported by @akarichan8231 in Discord on 2026-05-04.

42 regression tests across:
  tests/test_issue1623_sse_heartbeat_alignment.py (3)
  tests/test_issue1624_repair_stale_pending_grace.py (9)
  tests/test_issue1625_local_server_model_id_preservation.py (30)

4142 -> 4184 passing. 0 regressions.
2026-05-04 16:49:43 +00:00
nesquena-hermes 25cb35ee1a Merge pull request #1632 from nesquena/stage-293
Release v0.50.293 — 3-PR batch (profile isolation trio + agent version + #1597 follow-up)
2026-05-04 09:36:37 -07:00
nesquena-hermes 6bc0f9c4d5 Apply Opus pre-release SHOULD-FIX + NITs (in-PR per release policy)
SHOULD-FIX #1 (renamed-root client cross-alias): drop strict-equality client
filter at static/sessions.js:1853. Server-side _profiles_match cross-aliases
'default'-tagged rows to a renamed root 'kinni'; the strict-equality client
would reject them, dropping every legacy session for renamed-root users. The
server is now solely authoritative for profile scoping.

SHOULD-FIX #2 (messaging-source dedupe ordering): _keep_latest_messaging_session_per_source
now runs AFTER the profile filter at api/routes.py:2078. Before, it ran on
the merged-cross-profile list with profile-blind keys, discarding the older
profile's row across profiles before the scope filter — leaving zero rows for
any messaging identity the active profile shared with another profile.

NIT #3: _projects_migrated flag now set only AFTER successful save_projects.
NIT #4: cleaned dead test code in test_is_root_profile_invalidation_drops_stale.
NIT #5: _create_profile_fallback's clone_from=='default' literal now routes
through _is_root_profile() for parity with the 5 other callsites.

+2 regression tests pin the SHOULD-FIX shapes:
- test_keep_latest_messaging_runs_after_profile_filter (source-string ordering)
- test_static_sessions_js_trusts_server_profile_scoping (no client re-filter)

4173 -> 4175 tests pass. 0 regressions.
2026-05-04 16:17:26 +00:00
nesquena-hermesandstefanpieter e8862632ed fix(profiles): scope sessions, projects, and root-profile resolution to active profile (#1611, #1612, #1614)
Closes #1611 — /api/sessions filters by active profile by default; ?all_profiles=1
opt-in for aggregate views; new _profiles_match() helper honours renamed-root
cross-aliasing; static/sessions.js drops the s.is_cli_session bypass; toggle-on
re-fetches with all_profiles=1 instead of slicing client-cached rows.

Closes #1612 — new _is_root_profile() central helper consults list_profiles_api()
for is_default=True matches alongside the legacy 'default' alias. Replaces five
literal-default callsites in api/profiles.py. Memoized with explicit invalidation
hooks at create + delete. Sticky active_profile file write now stores '' for
renamed root, consistent with the legacy empty==root contract.

Closes #1614 — projects carry a profile field stamped at create-time;
/api/projects filters by active profile; /api/projects/{create,rename,delete}
and /api/session/move reject ops on cross-profile projects with 404; new
_PROJECTS_MIGRATION migration in load_projects() back-tags untagged projects
from any session that uses them, fall back to 'default'; ensure_cron_project
keys lookup by (name, profile) so each profile gets its own Cron Jobs project.

31 regression tests (9+11+11) pin the renamed-root resolution, server-side
profile scoping shape, helper invariants, cross-alias matching, migration
behavior, and active-profile guards on every project mutation endpoint.
4148 tests pass.

Reporter: @stefanpieter

Co-authored-by: stefanpieter <noreply@github.com>
2026-05-04 16:03:05 +00:00
nesquena-hermes 95200419ee Merge pull request #1626 from nesquena/stage-292
Release v0.50.292 — 12-PR batch (multi-tab SSE + subpath routes + 3 follow-ups + UX polish)
2026-05-04 08:50:46 -07:00
nesquena-hermes 9986d2fd30 Merge pull request #1596 from nesquena/stage-291
Release v0.50.291 — 'What's new?' link 404 fix (closes #1579)
2026-05-03 22:32:35 -07:00
nesquena-hermes 3369a08f37 fix(updates): use merge-base for compare URL so 'What's new?' link resolves
Closes #1579.

api/updates.py was building the GitHub compare URL from local HEAD short SHA:

    repoUrl + '/compare/' + curSha + '...' + newSha
    where curSha = `git rev-parse --short HEAD`

Whenever local HEAD diverges from upstream — unpushed work, dirty stage
branches, forks, in-flight rebases, release-time merge commits whose SHA
only lives in the maintainer's local history — the compare URL points at
a SHA github.com has never seen and returns the standard 404 page.

Reporter (@ai-ag2026) observed:
  https://github.com/nesquena/hermes-webui/compare/c660c7f...86cb22e
  → 404 because c660c7f was an unpushed local commit.

The right base is `git merge-base HEAD <compare_ref>` — the most recent
commit local and upstream share. Since `git fetch` succeeded just before,
the merge-base is guaranteed to exist on the upstream GitHub repo.

Behavior matrix:
  Pure-behind clone (no local commits): merge-base == HEAD; URL unchanged.
  Behind + local-only commits (#1579):  merge-base != HEAD; URL points at
                                        public ancestor instead of local HEAD.
  merge-base failure (shallow clone):   current_sha=None; JS link guard
                                        suppresses link rather than emitting
                                        a known-broken URL.

Also hardens static/ui.js: reset the link's href and display:none on every
banner render, so a stale link from a prior render can't survive a re-render
where the new payload has current_sha=null.

Tests:
  - test_current_sha_is_merge_base_not_local_HEAD — reporter's scenario
  - test_current_sha_equals_HEAD_when_no_local_commits — backward compat
  - test_current_sha_falls_back_to_None_when_merge_base_fails — defensive
  - test_whats_new_link_resets_display_and_href_on_every_render
  - test_whats_new_link_suppressed_when_curSha_falsy
  - test_reporter_url_shape_no_longer_produces_invalid_compare_url

4094 → 4100 passing. 0 regressions.
2026-05-04 05:26:19 +00:00
nesquena-hermes 45591638a9 Merge pull request #1593 from nesquena/stage-290
Release v0.50.290 — 5-PR batch (login cache + sidebar UX + workspace dropdown polish)
2026-05-03 22:12:22 -07:00
nesquena-hermes bf7bc6b4c4 Merge pull request #1582 from nesquena/stage-289
Release v0.50.289 — TCP keepalive on accepted connections (#1581)
2026-05-03 16:52:08 -07:00
nesquena-hermes 86cb22e04b Merge pull request #1577 from nesquena/stage-288
v0.50.288 — picker symmetry + cron profile isolation (3 PRs)
2026-05-03 15:56:46 -07:00
nesquena-hermesandkowenhaoai df03055def Address review feedback: tighten profile-resolution error handling
Three small follow-ups from the review:

1. Remove the over-broad except Exception around get_active_hermes_home()
   in _handle_cron_run. The function is in-memory dict reads + one
   Path.is_dir() stat — if it raises from inside a request handler,
   api.profiles is in a state we shouldn't be making cron decisions in.
   A silent fallback to _profile_home=None re-introduces the exact
   bug #1573 fixes (worker thread runs unpinned against process-global
   HERMES_HOME). Better to 500 the request than risk silent cross-
   profile state corruption.

2. Add a thread-safety note on os.environ mutation in api/profiles.py
   explaining why _cron_env_lock is sufficient — CPython env-var
   assignment is GIL-protected at the bytecode level but the multi-step
   read-modify-write pattern (snapshot prev → assign new → restore on
   exit) is not atomic without explicit serialization. The lock makes
   the entire context-manager body run-to-completion serially, including
   any subprocess.Popen() calls inside run_job() that inherit the env.

3. New regression test (test_cron_run_does_not_silently_swallow_profile_resolution_errors)
   pinning the no-silent-fallback contract via source-level assertion.
   Catches future re-introduction of the over-broad except clause.

Co-authored-by: kowenhaoai <kowenhaoai@users.noreply.github.com>
2026-05-03 22:29:57 +00:00
nesquena-hermes 458cf38ac9 fix(picker): collapse duplicate provider groups + guard provider-id-as-model.default (closes #1568)
Reporter (Deor, Discord #report-bugs, May 03 2026 14:19 PT, relayed by
@AvidFuturist) saw the Settings → Default Model dropdown rendering the
OpenCode Go provider as TWO separate optgroups: "OpenCode Go" (the
canonical one with all 14 catalog models) and "Opencode_Go" (a phantom
group containing one self-referential entry).

Three structural causes, all in api/config.py:_build_available_models_uncached:

1. **Detection-path id leakage.** The detection block at line ~1980
   reads cfg["providers"] keys verbatim. If the user's config has
   ``providers.opencode_go.api_key`` (underscore variant) AND another
   path adds the canonical ``opencode-go`` (e.g. via active_provider),
   both end up in detected_providers and the build loop creates two
   distinct provider groups with the second labelled via the
   ``pid.title()`` fallback as ``"Opencode_Go"``.

2. **Injection-block rogue model.** The default-model injection block
   at line ~2598 puts ANY ``model.default`` string into the picker as
   a fake option. A stray ``model.default: opencode_go`` (provider id
   mistakenly used as a model id) surfaces as a phantom model
   labelled ``"Opencode GO"``.

3. **Empty-group bleed.** When a non-canonical provider id makes it
   into detected_providers but has no entry in _PROVIDER_MODELS, the
   build loop creates an optgroup with zero models — pure UI noise.

This PR addresses all three:

- **New `_canonicalise_provider_id()` helper** that folds underscores
  to hyphens, lowercases, and applies alias resolution only when the
  alias target is itself a canonical id in `_PROVIDER_DISPLAY`. The
  last constraint avoids round-tripping ``x-ai`` (canonical) through
  the alias table to ``xai`` (which the WebUI doesn't index by).

- **Detection-path canonicalisation.** The cfg["providers"] scan
  applies the helper before adding to detected_providers. Same
  treatment in the only_show_configured intersection so that mode
  doesn't accidentally exclude the canonical id when configured_providers
  only contains the underscore-variant key.

- **Post-collection dedup pass** that re-canonicalises every entry in
  detected_providers — belt-and-braces against future regressions in
  any of the ~25 ``detected_providers.add(...)`` callsites without
  auditing each one. Idempotent for already-canonical ids.

- **Provider-id guard on the model.default injection block.** When
  the injected value matches a known provider display name or alias
  (after underscore/case normalisation), skip the injection and emit
  a `logger.warning` instead. Real unknown model ids (newly released
  models, custom endpoints) still get injected — only provider-shaped
  values are rejected.

- **Empty-group filter at end of build.** Drop optgroups with zero
  models. Custom: groups (`provider_id` starts with `custom:`) are
  exempt — users may want an empty card visible as a reminder.

Tests
-----

`tests/test_issue1568_duplicate_provider_groups.py` (17 tests):

- TestCanonicaliseProviderId (8): unit tests pinning helper behaviour —
  canonical preserved, underscore folded, case folded, aliases
  resolved, x-ai not round-tripped, empty input, unknown ids
  normalised, idempotence
- TestProviderGroupDedup (4): end-to-end picker behaviour —
  underscored providers-key produces ONE group not two (Deor's case),
  uppercase providers-key collapsed, aliased keys (z-ai → zai)
  collapsed, happy path unchanged
- TestDefaultModelProviderIdGuard (3): provider id as model.default
  doesn't inject phantom + WARNING logged; alias as model.default also
  caught; legitimate unknown model IDs (forward-compat) still injected
- TestEmptyGroupFilter (2): empty optgroups dropped from picker;
  custom: providers exempted from filter

Plus one structural test fix in
`tests/test_issue604_all_providers_model_picker.py:test_cfg_providers_only_adds_known`
— widened the regex window from 500 to 1500 chars so the new
documentation comment block doesn't push `_PROVIDER_MODELS` past the
substring slice. Pre-existing brittle window pattern, not a new issue.

Verification
------------

Live on port 8789 with Deor's exact reproduction config
(`providers.opencode_go.api_key` + `model.provider: opencode-go`):

  /api/models groups: 1 (was 2)
  Browser <select> optgroups: 1 (was 2)
  Total options under "OpenCode Go": 14 (was 14 in real group + 0 in phantom group)

Five-scenario sweep all collapse to ONE provider group:

| Config shape | Pre-fix | Post-fix |
|---|---|---|
| Hyphenated provider + underscored providers-key (Deor's case) | 2 groups | 1 group ✅ |
| Hyphenated provider + UPPERCASE providers-key | 2 groups | 1 group ✅ |
| Aliased providers-key (z-ai resolved to zai) | 2 groups | 1 group ✅ |
| model.default = provider-id (orig #1568 scenario) | 15 models with phantom | 14 models, no phantom ✅ |
| Happy path (canonical-only) | 1 group | 1 group ✅ |

4070 pytest passed (was 4053 → 4070, +17 from this PR).
3 CI runs to follow on push.
QA harness 11/11 passed.
JS unaffected — pure backend fix.

Reporter: Deor (Discord #report-bugs, May 03 2026 14:19 PT)
Relayed by: @AvidFuturist
2026-05-03 22:04:58 +00:00
nesquena-hermes a2b793be4f fix(picker): Nous Portal featured-set cap + endpoint symmetry (closes #1567)
Two related dropdown bugs in one PR — same root shape (model-picker
endpoints disagreeing about which Nous Portal models exist) plus the
preemptive UX guard against the picker becoming unusable on large-tier
Nous accounts.

#1567 — Endpoint disagreement
=============================
Reporter (Deor, Discord, May 03 2026) saw Settings → Providers card
showing "Nous Portal — 396 models · OAuth" while the in-conversation
picker dropdown listed only the four hardcoded curated entries.

Two structural causes:

1. ``api/providers.py:get_providers`` iterates ALL OAuth providers
   regardless of authentication state and unconditionally live-fetches
   the catalog.
2. ``api/config.py:_build_available_models_uncached`` only iterates
   providers in ``detected_providers``, gated on
   ``hermes_cli.models.list_available_providers().authenticated``.
   That flag can disagree with ``get_auth_status(<id>).logged_in`` on
   some hermes_cli versions.

When the disagreement happens for Nous, the picker silently falls
through to the curated 4-entry static list while the providers card
keeps showing the live catalog — exactly the asymmetry users report.

Plus: the Nous live-fetch branch in `_build_available_models_uncached`
fell back to the same curated 4-entry list when `provider_model_ids`
returned an empty list (transient failure / OAuth refresh in flight),
which doubles down on the disagreement instead of healing it.

UX cap (the design concern Nathan flagged on triage)
====================================================
Even with the disagreement fixed, dumping a 397-model catalog into a
flat dropdown is unusable. We trim the visible picker to a curated
~15-entry featured set when the catalog exceeds 25 models, and surface
the rest under a new ``extra_models`` field so:

- ``/model`` slash autocomplete (commands.js) covers the full catalog
- ``_dynamicModelLabels`` (ui.js) hydrates from both lists, so a model
  selected from outside the featured slice still gets a proper label
- The optgroup label gets ``" (15 of 397)"`` appended so the user
  understands the dropdown is intentionally trimmed, not broken
- The providers card surfaces ``models_total`` separately so the
  header still reads "397 models · OAuth"
- A small "+N more" disclosure pill appears at the end of the rendered
  pill list (only fires for non-OAuth providers — OAuth cards never
  render pills) with a tooltip pointing at the slash command

Featured selection rules
------------------------
Deterministic; same algorithm runs in both `/api/models` and
`/api/models/live` so background enrichment doesn't undo the trim:

1. Always include the user's currently-selected model (sticky — no
   orphan IDs in the dropdown after a refresh)
2. Always include every entry from the curated static
   ``_PROVIDER_MODELS["nous"]`` list whose id maps onto a live id
3. Top up to 15 by walking ``_NOUS_VENDOR_PRIORITY`` round-robin
   (one model per vendor each pass) so no vendor monopolises the slots

Changes by file
===============

api/config.py
- New `_format_nous_label` neighbour: `_NOUS_FEATURED_THRESHOLD = 25`,
  `_NOUS_FEATURED_TARGET = 15`, `_NOUS_VENDOR_PRIORITY` tuple,
  `_build_nous_featured_set()` helper (~80 LOC)
- `_build_available_models_uncached` Nous branch:
  - Apply featured-set cap with sticky-selection signal
  - Return `extra_models` alongside `models` for the catalog tail
  - Decorate optgroup label with truncation count
  - Drop stale-4 fallback when authenticated but live-fetch empty
    (omit the group entirely; truth lives in the providers card and
    the next cache rebuild will heal it)
  - Keep stale-4 fallback when hermes_cli is unavailable (test envs,
    package mismatches) — that's a different failure mode
- Detection symmetry: explicit `get_auth_status("nous").logged_in`
  check after the existing `list_available_providers()` loop, so the
  picker matches the providers card on hermes_cli versions where the
  two signals disagree

api/providers.py:get_providers
- Apply same featured-set cap so card body doesn't render 397 pills
- Add `models_total` field reporting full catalog size (used by
  frontend for the "N models · OAuth" header text)

api/routes.py:_handle_live_models
- Apply same featured-set cap for `/api/models/live` so background
  enrichment via `_fetchLiveModels()` doesn't undo the dropdown trim
- Use sticky-selection from `cfg["model"]["model"]` matching the main
  endpoint's logic

static/ui.js:populateModelDropdown
- Hydrate `_dynamicModelLabels` from `g.extra_models` so a selection
  outside the visible dropdown still renders with its proper label

static/commands.js:_loadSlashModelSubArgs
- Iterate `group.extra_models` so `/model` autocomplete covers the
  full catalog (not just the trimmed featured slice)

static/panels.js:_buildProviderCard
- Header count uses `p.models_total` (full catalog size) instead of
  `p.models.length` (trimmed slice)
- Render trailing "+N more" disclosure pill when `models.length <
  models_total` with a tooltip pointing at the slash command

static/style.css
- New `.provider-card-model-tag-more` rule (italic, dashed border,
  cursor:help, no select) — visually distinct from real model pills

Tests
=====

`tests/test_issue1567_nous_picker_capacity_and_symmetry.py` (20 tests):

- TestBuildNousFeaturedSet (8): unit tests on the helper —
  small-catalog no-op, large-catalog cap to target, disjoint+complete
  invariants, priority-vendor round-robin guarantee, sticky selection
  with and without `@nous:` prefix, curated-flagship preservation,
  empty-catalog handling, determinism
- TestApiModelsLargeCatalog (2): /api/models cap behavior end-to-end
  on a synthetic 397-model catalog vs a 20-model catalog
- TestNousDetectionSymmetry (2): picker includes Nous when
  `get_auth_status` agrees but `list_available_providers` disagrees;
  picker omits Nous when both disagree
- TestNousLiveFetchEmpty (2): authenticated + empty-fetch omits group;
  hermes_cli unavailable still falls back to static-4
- TestProvidersCardPickerSymmetry (1): both endpoints agree on
  exactly the same featured-set IDs + total catalog count
- TestFrontendExtrasContract (4): static-source assertions pinning
  the JS contract for `extra_models`, `models_total`, and the "+N more"
  disclosure

Verified live on port 8789 (30-model catalog):
- /api/models Nous group: provider="Nous Portal (15 of 30)", 15 models,
  15 extra_models
- /api/models/live?provider=nous: 15 entries (matches main path)
- /api/providers Nous card: models_total=30, models=15
- Browser dropdown after backfill: 15 options, 30 entries in
  _dynamicModelLabels
- Sticky selection: Claude Opus 4.7 (the active model) in the featured
  slice as expected

4073 pytest passed (was 4053 → 4073, +20 from this PR).
3 CI test runs (3.11/3.12/3.13) green.
QA harness 11/11 passed.

Reporter: Deor (Discord #report-bugs, May 03 2026 14:15 PT)
Relayed by: AvidFuturist
2026-05-03 21:44:22 +00:00
nesquena-hermes 70f86d56f4 Merge pull request #1566 from nesquena/stage-287
v0.50.287 — Self-update active-stream guard (#1565 by @ai-ag2026)
2026-05-03 14:20:58 -07:00
nesquena-hermes 75ec7db2df Merge pull request #1564 from nesquena/stage-286
v0.50.286 — Settings password field env-var lock UI (closes #1560)
2026-05-03 14:11:20 -07:00
nesquena-hermes 84cfc2f4cf Merge pull request #1563 from nesquena/fix/session-recovery-skip-non-session-json
v0.50.285 — same-day hotfix: session recovery actually fires now (closes #1558 follow-up)
2026-05-03 13:55:29 -07:00
nesquena-hermes dcf6467c6f Merge pull request #1562 from nesquena/stage-284
v0.50.284 — P0 data-loss hotfix + stale-stream race (closes #1533, #1558)
2026-05-03 13:44:43 -07:00
nesquena-hermes 47ba95fa92 Merge pull request #1556 from nesquena/stage-283
v0.50.283 — full PR sweep (8 PRs, 7 issues closed)
2026-05-03 12:32:55 -07:00
nesquena-hermes ac46239acd Merge pull request #1544 from nesquena/fix-nous-models-and-provider-removal-staleness
fix(providers): Nous Portal full live catalog + dropdown cache invalidation on provider remove (#1538, #1539)
2026-05-03 11:23:05 -07:00
nesquena-hermes 8fab43b3fe docs(release): stamp v0.50.282 — CHANGELOG + ROADMAP + TESTING test counts 2026-05-03 18:17:56 +00:00
nesquena-hermes c21e3086a2 docs: align _format_nous_label docstring examples with actual output
Per review observation on PR #1544: the docstring claimed
'Gemini 3.1 Pro Preview' and 'Nemotron 3 Super 120B A12B' but the
helper reuses _format_ollama_label's 3-letter-token rule, which
uppercases 'PRO' (and the existing rule for tokens like 'a12b'
renders 'A12b' not 'A12B'). Update the examples to match actual
behavior — labels are unchanged, only the docstring.

Pure-comment change, no behavioral effect. Test counts unchanged
(4013 passed).
2026-05-03 18:12:01 +00:00
nesquena-hermes bff8cb2b58 fix: Nous Portal full live catalog + dropdown cache invalidation on provider remove
Closes #1538, #1539. Two related dropdown-staleness bugs reported by Deor
(Discord, May 03 2026).

#1538 — Nous Portal picker showed only 4 hardcoded models
=========================================================
The Settings → Default Model picker, the composer model dropdown, the
/model slash command, and the Settings → Providers card all showed only
four Nous models (Claude Opus 4.6, Claude Sonnet 4.6, GPT-5.4 Mini, Gemini
3.1 Pro Preview) because `_PROVIDER_MODELS["nous"]` had four hardcoded
entries and `_build_available_models_uncached()` fell through to the
generic `pid in _PROVIDER_MODELS` branch.

The actual Nous Portal catalog has 30 models live — Claude Opus 4.7, GPT-5.5,
Kimi K2.6, MiniMax M2.7, Gemini 3.1 Pro/Flash, several Xiaomi/Tencent/StepFun
entries, and more.

Fix:
- New `_format_nous_label()` helper in `api/config.py` — reuses the
  `_format_ollama_label()` token rules, drops the vendor namespace, and
  appends ` (via Nous)` so labels disambiguate from same-named direct-
  provider entries (e.g. "Claude Opus 4.7" via direct Anthropic).
- New `elif pid == "nous":` branch in `_build_available_models_uncached()`
  mirroring the Ollama Cloud pattern: live-fetch through
  `hermes_cli.models.provider_model_ids("nous")`, prefix every id with
  `@nous:` (matches the existing routing convention from PR-era #854 and
  pinned in tests/test_nous_portal_routing.py), fall back to the curated
  4-entry static list when hermes_cli is unavailable.
- Same fix applied to `api/providers.py:get_providers()` — that's the
  separate code path that builds Settings → Providers card models, and
  it had the identical bug shape.

#1539 — Removed provider lingered in dropdowns until restart
============================================================
After Settings → Providers → Remove, the provider's models still appeared
in every model dropdown until the page was reloaded. The server-side
TTL cache was correctly flushed (`set_provider_key()` calls
`invalidate_models_cache()` on both add and remove) but JS-side caches
were never dropped:

- `_slashModelCache` / `_slashModelCachePromise` (commands.js) — feeds
  the `/model` slash-command suggestions.
- `_dynamicModelLabels` / `window._configuredModelBadges` (ui.js) —
  populated by `populateModelDropdown()` on app boot and profile switch.

Pre-fix, `_removeProviderKey()` only called `loadProvidersPanel()`
which refreshed the providers card list but never asked any consumer
to re-fetch /api/models.

Fix:
- `static/commands.js`: new `_invalidateSlashModelCache()` helper that
  nulls both cache slots, exposed on `window` (typeof-guarded so the
  module remains importable in headless vm contexts — needed by the
  existing tests/test_cli_only_slash_commands.py harness).
- `static/panels.js`: new `_refreshModelDropdownsAfterProviderChange()`
  helper that calls the invalidator + `populateModelDropdown()`, wrapped
  in try/catch so the providers panel update never breaks if a
  downstream module hasn't loaded yet. Both `_saveProviderKey` and
  `_removeProviderKey` invoke it (defense-in-depth: same staleness shape
  applies to the add path too).

Tests
-----
- `tests/test_issue1538_nous_live_catalog.py` (12 tests): live-fetch
  surfaces ≥20 entries, every id starts with `@nous:`, every label ends
  with ` (via Nous)`, recent flagships (Opus 4.7, GPT-5.5, Kimi K2.6,
  Gemini 3.1 Pro, MiniMax M2.7) reach the dropdown, static fallback
  works when hermes_cli raises, label formatter unit tests (vendor
  namespace stripping, variant rendering, MiniMax mixed-case), the
  curated static list and its routing invariants are preserved.
- `tests/test_issue1539_provider_removal_dropdown_invalidation.py`
  (11 tests): invalidator helper exists and clears both cache slots,
  exposed on window with typeof guard, both save and remove paths
  invoke the dropdown flush, helper calls both invalidator and
  populateModelDropdown, helper is resilient to missing modules,
  helper does not block panel refresh, server-side
  `set_provider_key → invalidate_models_cache` invariant pinned.

Verified live on port 8789: `/api/models` Nous group returns 30
models (was 4); browser `document.getElementById('modelSelect')`
exposes 30 options under the "Nous Portal" group; the dropdown-flush
helper is callable from the browser and round-trip rebuild keeps the
dropdown at 30 options.

Test counts:
- Full pytest: 4013 passed, 2 skipped, 3 xpassed, 0 failures
  (was 3990 → 4013, +23 from this PR).
- QA harness pytest: 20 passed.
- Browser API sanity: 11/11 passed.
- Agent Browser CDP: 21/23 passed (the 2 SSE liveness failures
  reproduce on master and are unrelated to this PR).
2026-05-03 18:12:01 +00:00
nesquena-hermes 84e74407c9 Merge pull request #1542 from nesquena/docs/roadmap-sprints-refresh
docs: rewrite ROADMAP.md and SPRINTS.md for v0.50.281 currency
2026-05-03 10:41:38 -07:00
nesquena-hermes 99bf3f4aeb Merge pull request #1541 from nesquena/stage-281
v0.50.281 — LM Studio config-driven classification (#1536 by @dutchaiagency)
2026-05-03 10:17:40 -07:00
nesquena-hermes 3964339a58 Merge pull request #1540 from nesquena/stage-280
v0.50.280 — Cross-channel messaging handoff (#1404) + reasoning-effort salvage (#1535)
2026-05-03 09:58:43 -07:00
nesquena-hermes f8ed6dac05 Merge pull request #1534 from nesquena/stage-279
v0.50.279 — 8-PR batch from full PR sweep + Opus MUST-FIX caught
2026-05-03 09:26:03 -07:00
nesquena-hermes 9e31a2ac65 Merge pull request #1521 from nesquena/stage-278
v0.50.278 — sidebar Unassigned filter chip (splices #1497 + #1513)
2026-05-03 00:17:17 -07:00
nesquena-hermes 7921a47f9d Merge pull request #1515 from nesquena/stage-277
v0.50.277 — model-picker shared-reference fix (supersedes #1511)
2026-05-02 23:50:17 -07:00
nesquena-hermes 8ef58cad27 Merge pull request #1510 from nesquena/stage-276
v0.50.276 — SW stale-CSS fix (PR #1508, closes #1507)
2026-05-02 23:28:34 -07:00
nesquena-hermes 52226bcdd7 Merge pull request #1506 from nesquena/stage-275
v0.50.275 — /session/static/* MIME-type fix (PR #1505 by @rickchew)
2026-05-02 22:29:52 -07:00
nesquena-hermes 539a72b9e6 Merge pull request #1504 from nesquena/stage-274
Stage 274: PR #1501 — LM Studio onboarding fully fixed (probe + keyless + LM_API_KEY alignment) (closes #1499 #1500)
2026-05-02 20:34:56 -07:00
nesquena-hermes 9b8d0bac0c Merge pull request #1498 from nesquena/fix/lmstudio-provider-env-var-1420
fix(providers): map lmstudio to LMSTUDIO_API_KEY in _PROVIDER_ENV_VAR (#1420)
2026-05-02 19:16:13 -07:00
nesquena-hermes 6c3ff3ff47 Merge pull request #1496 from nesquena/stage-272
Stage 272: 3 PRs — #1493 sidebar cancel + #1495 state.db FD leak fix + #1492 P0 polish bundle (closes #1466 #1469 #1484 #1486 #1494; refs #1458 Bug #2)
2026-05-02 18:41:16 -07:00
nesquena-hermes 7fddc331ae Merge pull request #1490 from nesquena/stage-271
v0.50.271 — Composer voice buttons UX (#1488)
2026-05-02 15:37:22 -07:00
nesquena-hermes 913c93ae85 Merge pull request #1487 from nesquena/stage-270
v0.50.270 — Bootstrap launcher import validation (#1315) + Opus follow-up
2026-05-02 12:56:25 -07:00
nesquena-hermes b8a346f421 Merge pull request #1483 from nesquena/stage-269
v0.50.269 — Bootstrap supervisor fix (#1478) + #1473 follow-ups (#1479, #1480)
2026-05-02 11:14:23 -07:00
nesquena-hermes 7d5c9bd76f Merge pull request #1482 from nesquena/stage-268
v0.50.268 — 4 contributor PRs (sessions URL sync, sidebar nesting, /api/session/duplicate, Android PWA) + Opus follow-ups
2026-05-02 10:57:08 -07:00
nesquena-hermes 5650d1107a Merge pull request #1475 from nesquena/stage-267
v0.50.267 — 7 contributor PRs (model ID normalization, navigation, sessions, batch actions) + Opus follow-up
2026-05-02 10:05:24 -07:00
nesquena-hermes 4e0dce9a03 Merge pull request #1449 from nesquena/polish-v265-followups
v0.50.264 polish followups: i18n parity + assistant-output readability (closes #1442, #1443, #1446, #1447)
2026-05-01 21:23:38 -07:00
nesquena-hermes 0ed6103f1e Merge pull request #1448 from nesquena/stage-265
release: v0.50.265 — opt-in WebUI extension hooks
2026-05-01 20:53:08 -07:00
nesquena-hermes fb66ba5e10 Merge pull request #1444 from nesquena/stage-264
release: v0.50.264 — ja locale, IME Safari fix, fence regex anchoring
2026-05-01 20:11:08 -07:00
nesquena-hermes 9d0d86be5f Merge pull request #1437 from nesquena/fix/issue-1436-context-indicator-load-path
fix: context-window indicator broken on older sessions (#1436)
2026-05-01 18:54:13 -07:00
nesquena-hermes c8f2daa990 Merge pull request #1435 from nesquena/fix/profile-autocapitalize-and-newchat-guard
fix: new-chat guard ignores in-flight streams (#1432) + profile form auto-capitalizes (#1423)
2026-05-01 18:04:09 -07:00
nesquena-hermes 0dd4dd39c4 Merge pull request #1434 from nesquena/stage-261
v0.50.261: composer-footer toolsets chip responsive (replaces #1433)
2026-05-01 17:23:38 -07:00
nesquena-hermes ee3717a758 Merge pull request #1429 from nesquena/stage-260
v0.50.260 — Docker reliability overhaul (PR #1428 + UX/docs + Opus follow-up)
2026-05-01 16:12:46 -07:00
nesquena-hermes c0d50b3828 Merge pull request #1427 from nesquena/stage-259
v0.50.259 — SessionDB FD-leak hotfix (#1421) + LRU-eviction Opus follow-up
2026-05-01 15:46:27 -07:00
nesquena-hermes 2ae07ba906 Merge pull request #1422 from nesquena/stage-258
v0.50.258 — login stability batch (#1419) + redirect-encoding Opus follow-up
2026-05-01 15:30:49 -07:00
nesquena-hermes 101c2b47c5 Merge pull request #1417 from nesquena/stage-257
v0.50.257 — batch release: 2 PRs (#1402 + #1415) + 5 Opus follow-ups (1 CRITICAL)
2026-05-01 12:04:16 -07:00
nesquena-hermes 29f77c4b6e Merge pull request #1414 from nesquena/fix-tts-volume-icon
fix: register 5 missing Lucide icons (TTS speaker + queue chevron + insights cards) (#1413)
2026-05-01 11:01:24 -07:00
nesquena-hermes 101d02a3e9 Merge pull request #1412 from nesquena/stage-255
v0.50.255 — batch release: 2 PRs (#1390 + #1405) + 4 Opus follow-ups
2026-05-01 10:40:10 -07:00
nesquena-hermes 4674f383af Merge pull request #1408 from nesquena/stage-254
v0.50.254 — batch release: 4 PRs + Opus follow-up
2026-05-01 09:45:29 -07:00
nesquena-hermes e258672bcb fix(sessions): instant mouse click navigation, preserve tap-vs-drag cancel on touch
Clicking a chat in the sidebar now processes immediately when using a mouse or
trackpad, but introduces a 300ms delay on touch devices to prevent accidental
navigation when a user scrolls the sidebar and lifts their finger mid-gesture.

Drag is detected when the pointer moves more than 5px from the pointerdown
position; a detected drag cancels any pending tap on release and suppresses
the hover highlight via a .dragging class added synchronously and removed
after a 50ms defer to prevent :hover activating before class removal settles.
The double-tap-to-rename path is unaffected.

Detection uses e.pointerType (already available on the pointerup event) rather
than user-agent sniffing.
2026-05-01 10:42:32 +01:00
nesquena-hermes 219f5d6ce5 Merge pull request #1391 from nesquena/stage-may2
release: v0.50.253 — /branch command + local-provider heal + mobile composer + Opus follow-ups
2026-05-01 00:02:53 -07:00
nesquena-hermes b5009dd5b4 Merge pull request #1387 from nesquena/stage-may1
release: v0.50.252 — 6 fork PRs (#1377 #1378 #1379 #1380 #1382 #1386) + Opus follow-ups
2026-04-30 22:10:47 -07:00
nesquena-hermes 031feda376 Merge pull request #1371 from nesquena/release/v0.50.251
release: v0.50.251
2026-04-30 16:50:44 -07:00
nesquena-hermes ffd11037b1 Merge pull request #1368 from nesquena/release/v0.50.250
release: v0.50.250
2026-04-30 15:49:58 -07:00
nesquena-hermes d72399ae22 Merge pull request #1365 from nesquena/release/v0.50.249
release: v0.50.249
2026-04-30 15:02:27 -07:00
nesquena-hermes 9303636dd9 Merge pull request #1351 from nesquena/release/v0.50.248
release: v0.50.248
2026-04-30 11:50:17 -07:00
nesquena-hermesandnesquena-hermes 880350312a fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up) (#1348)
* fix(streaming): fallback to model_metadata for context_length when compressor missing (#1318 follow-up)

PR #1318 (shipped in v0.50.246 via PR #1341 + commit a5c10d5) persisted
context_length on the session so the context-ring indicator survives
page reloads. But the writer only fired when agent.context_compressor
was present and reported a non-zero value. Fresh agents, interrupted
streams, or compressors without the attribute would still leave
s.context_length=0 — and the indicator would still show 0% on reload.

This follow-up adds a fallback that calls
agent.model_metadata.get_model_context_length(model, base_url) when the
compressor didn't populate the value. The function returns a sensible
static context window for any known model (with a 256K default for
unknown models). Wrapped in a broad try/except because older
hermes-agent builds may not expose the helper.

Sourced from PR #1344 (@jasonjcwu) — extracted into this focused
follow-up after #1344 was closed as superseded by #1341.

Adds 6 structural tests covering: import + call presence, falsy-gate,
agent.model/base_url passing, exception swallowing, save() ordering,
result assignment.

Closes the data-flow gap in #1318 for the compressor-missing case.

* test: relax pr1341 block-size assertion to accommodate the new fallback

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-30 10:27:56 -07:00
nesquena-hermes f70b791bf8 Merge pull request #1347 from nesquena/release/v0.50.247
release: v0.50.247 — Cron Jobs project auto-assignment (#1345)
2026-04-30 10:26:56 -07:00
nesquena-hermes dec4b48607 Merge pull request #1343 from nesquena/release/v0.50.246
release: v0.50.246 — 5-PR batch
2026-04-30 09:49:14 -07:00
nesquena-hermes 280ab86480 Merge pull request #1340 from nesquena/chore/markdown-refresh-v0.50.245
docs: refresh markdown to v0.50.245 + add CONTRIBUTORS.md
2026-04-30 09:15:31 -07:00
nesquena-hermes 52e1567bd1 Merge pull request #1334 from nesquena/release/v0.50.245
release: v0.50.245 — 10-PR batch
2026-04-30 08:48:53 -07:00
nesquena-hermes 3f838fc31a release: v0.50.244 (#1308)
release: v0.50.244

Batch release of 4 PRs:

- #1303 (@fecolinhares) — TTS playback of agent responses via Web Speech API.
  Per-message speaker button + auto-read toggle + voice/rate/pitch in
  Settings. localStorage-only state. Closes #499.

- #1304 — Stale saved session 404 cleanup + structured api() errors.
  Salvaged from #1084. Independently approved on 358275e.

- #1306 — Cmd/Ctrl+K works while a conversation is busy.
  Salvaged from #1084. Independently approved on 2e8a239.

- #1307 — Sienna skin (warm clay & sand earth palette).
  Salvaged from #1084. Independently approved on 5cd79c8.

Tests: 3290 passed, 2 skipped, 3 xpassed, 0 failures (was 3254; +36 tests).

Independently reviewed and approved by nesquena (commit 47f0e0d). End-to-end
trace verified the TTS flow; security audit confirmed SpeechSynthesisUtterance
is plain-text-only with no XSS surface; behavioural harness confirmed
_stripForTTS handles all 12 markdown-stripping cases; bounds clamping on
rate/pitch verified; opt-in behavior verified.
2026-04-29 21:34:27 -07:00
nesquena-hermes ded9b7e1c4 release: v0.50.243 (#1302)
release: v0.50.243

Batch release of 2 PRs.

- #1301 — fix: remove PRIMARY chip badge + add Claude Opus 4.7 label
  Drops the chip-projected configured-model badge added in #1287 (chip
  width 235px → 164px). Adds Claude Opus 4.7 label entries so the picker
  no longer renders "Claude Opus 4 7" (missing dot).
  Independently reviewed and approved by nesquena (commit c0bbd23).

- #1297 (@franksong2702) — fix: preserve cron output response snippets
  Fixes #1295. /api/crons/output now preserves the ## Response section
  when a large skill dump appears in the prompt section; falls back to
  file tail when no marker exists.

Tests: 3254 passed, 2 skipped, 3 xpassed.

Independently reviewed and approved by nesquena (commit b262e4d).
2026-04-29 21:06:30 -07:00
nesquena-hermes 20ac6dfe5c release: v0.50.242 — revert assistant serif font + remove Calm theme (#1299)
Reverts the global assistant serif rule and removes the Calm theme that were shipped in v0.50.240 PR #1282. Pure deletion; 3252 tests passing. Override on independent review per Nathan.
2026-04-29 19:59:26 -07:00
nesquena-hermes 0ad95cb16a release: v0.50.241 (#1293)
release: v0.50.241

Batch release of 4 PRs:

- #1290 (@nickgiulioni1) — Inline audio/video media editor with playback
  speed controls and HTTP byte-range streaming. PDF/media previews in
  workspace file browser. Composer tray inline players for audio/video.
  (Rebased from #1232.)

- #1287 (@renatomott) — Configured model badges (Primary / Fallback N) in
  the model picker, carried through to the composer chip. Persists through
  on-disk model cache.

- #1289 (@franksong2702) — Appearance autosave for theme/skin/font-size in
  Settings; inline Saving / Saved / Failed status. Font size now persists
  to config.yaml. Refs #1003.

- #1294 (@franksong2702) — Normalize agent session source metadata
  (raw_source / session_source / source_label) through /api/sessions and
  gateway watcher SSE snapshots. Existing source_tag / is_cli_session
  fields preserved. Refs #1013.

Tests: 3254 passed, 2 skipped, 3 xpassed (was 3199 before this release).

Independently reviewed and approved by nesquena (commit d1738f6).
2026-04-29 19:54:07 -07:00
nesquena-hermes 33a145a669 release: v0.50.240
## Release v0.50.240

Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures).

---

### Added

- **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282
- **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482
- **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479
- **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485
- **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481
- **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568
- **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281
- **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268
- **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269

### Fixed

- **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266
- **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278
- **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267
- **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273

---

### Test results

```
3199 passed, 2 skipped, 3 xpassed in 72.79s
```

### PRs on hold (not included)

#1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-29 17:42:32 -07:00
nesquena-hermes 9f269a4f1c release: v0.50.239
h4-h6 heading fix. Approved by @nesquena. Tests: 3064 passed.
2026-04-29 09:07:03 -07:00
nesquena-hermes 9b6bce3a0d release: v0.50.238
Batch release — 12 PRs. Approved by @nesquena. Tests: 3061 passed.
2026-04-29 08:53:51 -07:00
nesquena-hermes 24b1e6f3fc fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)

Merges PRs #1208, #1209, #1210 (#1152 rebased):

- fix(providers): OAuth provider cards show correct Configured status in Settings.
  get_providers() was discarding has_key=True from _provider_has_key() for OAuth
  providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
  from the Settings panel. Surfaces auth_error string. (closes #1202)

- ux(profiles): profile chip shows spinner and new name immediately on switch.
  Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
  populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.

- feat: YOLO mode toggle — skip all approvals per session.
  /yolo slash command, "Skip all this session" button on approval cards,
  amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
  Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
  Original author: @bergeouss (PR #1152)

Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-27 22:56:12 -07:00