* fix(docker): register /usr/local/lib in ld.so.conf.d so the source-built SQLite loads on arm64
The #6900 SQLite-from-source upgrade passed the amd64 docker-smoke but FAILED the
multi-arch release build on linux/arm64 with 'AssertionError: SQLite 3.46.1 still
vulnerable' — Python's sqlite3 kept loading the base image's /usr/lib multiarch
libsqlite3 (3.46.1) instead of the freshly-compiled /usr/local/lib copy (3.53.0),
because /usr/local/lib is NOT in the default ld.so search path on Debian arm64
(it happened to be picked up on amd64). Register /usr/local/lib via an
ld.so.conf.d entry before ldconfig so the new lib wins on every architecture.
The PR's own build-time version assertion is what surfaced this (correctly), and
it also serves as the verification that the fix works once the arm64 build passes.
Follow-up to #6900 (exp-v0.52.227, whose multi-arch image failed to build/push).
* test-fix: keep wal.html link within 500 chars of ARG (condense arm64 comment)
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* feat(docker): enable gateway API and password forwarding in compose files
- Added API_SERVER_KEY forwarding to enable the gateway API listener in multi-container setups.
- Configured HERMES_WEBUI_PASSWORD to ensure password authentication works as expected.
- Updated documentation to clarify API_SERVER_KEY requirements and forwarding behavior.
- Added regression tests to cover the new functionality and ensure proper configuration.
* build(docker): force LF line endings for shell scripts via .gitattributes
A CRLF shebang (#!/bin/bash\r) breaks locally-built Docker images: the
kernel can't resolve the interpreter /bin/bash\r, so 'docker compose up
-d --build' on Windows with core.autocrlf=true fails with
'exec /hermeswebui_init.bash: no such file or directory'. The repo blobs
and CI/GHCR builds are LF and unaffected; only Windows local builds
baked the CRLF checkout into the image.
* fixup(docker): dedup API_SERVER_HOST after rebase onto #6968 (which already added it)
* docs(changelog): stamp #6917 (multi-container gateway API forwarding)
---------
Co-authored-by: Yang <yang_qwq@qq.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix(#6335): filter unconfigured providers from model selector
When a known provider (e.g. openai-api) is listed in config.yaml's
providers section with only metadata (name, display_name) and no route
configuration or credentials, it should not appear in the chat model
selector dropdown.
Previously, any provider recognised as a 'known provider' (present in
_PROVIDER_MODELS or _PROVIDER_DISPLAY) would be admitted to the picker
simply by being listed in the config.yaml providers block — even after
the user removed its API key. This caused a persistent ghost provider
entry in the dropdown (#6335).
The fix gates known-provider admission on credential evidence: the
provider must already have been detected from a credential source (env
vars, hermes auth, or credential pool) OR carry explicit route
configuration (api/base_url/api_key/key_env) OR be the active provider
with a configured models allowlist.
* fix(#6338): normalize provider aliases in credential check
The credential check compared _canonical (config key like 'x-ai',
'google') directly against detected_providers (alias-resolved values
like 'xai', 'gemini'). Alias-named authenticated providers were
therefore incorrectly treated as unconfigured and removed from the
model selector.
Fix: resolve _canonical through _resolve_provider_alias() before the
membership test, so config keys like 'x-ai' match credential evidence
reported as 'xai'. Also check the raw canonical for cases where the
config key is itself the alias target.
Refs: #6335, #6338
* fix(#6338): normalize detected_providers through alias resolver
Normalize both _canonical and detected_providers entries through
_resolve_provider_alias before the membership test, so that a WebUI
canonical form in detected_providers (e.g. x-ai added by a prior
loop iteration) also matches the alias-resolved form used in
credential detection (e.g. xai from hermes_cli).
This is belt-and-braces on the existing _resolve_provider_alias(_canonical)
check: both sides now map into the same alias-resolved namespace,
closing any edge case where detected_providers contains a WebUI-canonical
form that happens to differ from the alias target.
* test(ui): catalog-admission regression for credentialed providers (#6338)
* test(ui): pin config path state in catalog-admission regression (#6338)
* fix(system-health): expose runtime memory diagnostics without blocking owner locks (#6351)
* fix(system-health): keep owner contention local to runtime status (#6351)
* docs(changelog): stamp #6338#6363 (certified batch)
---------
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix(stream): preserve fade nodes while prose is live
* fix(stream): preserve transparent fade nodes like assistant prose
The transparent-activity prose path (transparent_stream mode) kept the
pre-PR fade cleanup: _bindTransparentFadeCleanup() in static/ui.js still
replaced every animated stream-fade-word span with a fresh text node on
animationend, invalidating the native scroll anchor while the row is
still growing.
Mirror the messages.js fix so both live prose paths share one
node-preserving cleanup: remove only the transient is-new class and the
--stream-fade-ms inline property, keeping span identity until final
settlement rebuilds the persisted DOM.
Extend tests/test_smooth_text_fade.py with a sibling regression that
extracts the production _bindTransparentFadeCleanup() from ui.js and
fires animationend, asserting no replacement, is-new removal,
stream-fade-word retention, and timing-override cleanup (plus
source-string guards against replaceWith in the cleanup block). The new
test is red-before-green against the previous ui.js implementation.
* docs(changelog): stamp #6257 (stream fade scroll-bounce fix)
---------
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix(renderer): exclude CJK full-width punctuation from autolink URLs
* fix(test): widen autolink block search window for CJK punctuation fix (#6792)
The CJK full-width punctuation fix expanded the autolink regex and
comment text, pushing target='_blank' and esc(clean) beyond the 600/
700-char search windows in tests. Widen to 1200 chars.
* fix(test): widen autolink window in test_issue470 to match 1200-char block
The CJK punctuation fix added comment lines that pushed href="${clean}"
beyond the 600-char search window in test_issue470.py::test_autolink_no_esc_on_href,
causing the same failure across 3.11/3.12/3.13 shard-4. test_issue342.py was
already widened in cc52202f; apply the same 1200-char window here.
* fix(test): drive real renderMd() for CJK full-width autolink behaviour
Replace the test-owned FIXED_REGEX copy in test_fullwidth_autolink.js with
a pytest + node harness that extracts and runs the ACTUAL renderMd() from
static/ui.js (same driver pattern as test_renderer_js_behaviour.py).
Coverage per review:
- every CJK trailing mark (),。;:!?、) through both the outer
paragraph pass and the inline pass (list items + blockquotes)
- ASCII trailing marks (regression guard)
- normal URLs with query params (href keeps raw &, text is escaped)
- Markdown links and pre-existing anchors are stashed, never re-wrapped
Reverse-verified: reverting the \uFF09 exclusion + CJK strip makes 27 of
38 assertions fail on the real production code.
* fix(test): drop stray f-prefix from placeholder-free asserts (F541)
* docs(changelog): stamp #6792 (CJK autolink punctuation)
---------
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix: include terminal backend identity in agent cache signature (#5937) (#6175)
* fix(mobile): preserve extension panels in drawer sync
* fix: close mobile sidebar drawer when opening main-view create forms
The add-space/new-profile/new-skill/new-cron forms render in the main
view, which on phones is covered by the full-screen sidebar drawer
(z-index 200). Opening one from inside the drawer only focused the
input (keyboard popped up) while the form stayed hidden underneath.
- openWorkspaceCreate / openProfileCreate / openSkillCreate /
openCronCreate now close the drawer after rendering the form,
mirroring their *_Detail siblings.
- move the fromRailClick drawer re-open in switchPanel() into the
synchronous section: it used to run after await loadXxxPanel(),
racing (and re-opening over) a drawer close issued from inside the
drawer during the panel load window.
Verified in Playwright (390x844): drawer closes and stays closed after
plus-click, form visible with focus on the path input; desktop (1280x800)
unchanged; 83 mobile + 26 workspace tests pass.
* fix: close mobile sidebar after project quick-create plus
The per-project '+' button (_attachProjectQuickCreateButton) created the
new project conversation but never closed the mobile sidebar drawer, so on
phones the tap looked like a no-op: the session was created, just hidden
underneath the full-screen drawer (z-index 200).
Close the drawer in the success path after the sidebar repaint, mirroring
$('btnNewChat').onclick in boot.js and the #5409 close in
_openSidebarSession. Failure path keeps the drawer open so the error toast
stays visible for retry.
Adds static regression tests pinning the guarded close call and its
position after the repaint.
* test: pin quick-create drawer close to the success path (try block)
Greptile review note: the string-presence and ordering assertions alone
would still pass if the guarded closeMobileSidebar call were moved into
the later catch branch, silently reintroducing the mobile drawer bug.
Add an explicit assertion that the close sits before the success-path
catch(err), so a regression that moves it into the failure branch is
caught.
* docs(changelog): stamp batch1-p1 (#6921#6933#6671#6175)
---------
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Rodrigo Gomes da Silva <rodrigo.smscom@gmail.com>
Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* 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>
* 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>
* 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>